diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 295dc54371d6..89d70f9fa3d7 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -128,7 +128,7 @@ In order to build the Umbraco source code locally, first make sure you have the * npm v6.4.1+ (installed with Node.js) * [Git command line](https://git-scm.com/download/) -The easiest way to get started is to open `src\umbraco.sln` in Visual Studio 2017 (version 15.9.7 or higher, [the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects). In Visual Studio, find the Task Runner Explorer (in the View menu under Other Windows) and run the build task under the gulpfile. +The easiest way to get started is to open `src\umbraco.sln` in Visual Studio 2019 (version 16.3 or higher, [the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects). In Visual Studio, find the Task Runner Explorer (in the View menu under Other Windows) and run the build task under the gulpfile. Alternatively, you can run `build.ps1` from the Powershell command line, which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. See [this page](BUILD.md) for more details. diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 3f512bc65a85..f829770a5962 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.8.0")] -[assembly: AssemblyInformationalVersion("8.8.0")] +[assembly: AssemblyFileVersion("8.9.0")] +[assembly: AssemblyInformationalVersion("8.9.0-rc")] diff --git a/src/Umbraco.Core/CompositionExtensions.cs b/src/Umbraco.Core/CompositionExtensions.cs index ced9a9386a56..2fcf446d6b8b 100644 --- a/src/Umbraco.Core/CompositionExtensions.cs +++ b/src/Umbraco.Core/CompositionExtensions.cs @@ -64,7 +64,7 @@ public static PropertyValueConverterCollectionBuilder PropertyValueConverters(th => composition.WithCollectionBuilder(); /// - /// Gets the url segment providers collection builder. + /// Gets the URL segment providers collection builder. /// /// The composition. public static UrlSegmentProviderCollectionBuilder UrlSegmentProviders(this Composition composition) diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 0b817cc0bbc4..2c79c0008655 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -68,9 +68,9 @@ public static bool HasSmtpServerConfigured(string appPath) internal static bool? HasSmtpServer { get; set; } /// - /// Gets the reserved urls from web.config. + /// Gets the reserved URLs from web.config. /// - /// The reserved urls. + /// The reserved URLs. public string ReservedUrls { get @@ -354,10 +354,10 @@ public string DefaultUILanguage } /// - /// Gets a value indicating whether umbraco should hide top level nodes from generated urls. + /// Gets a value indicating whether umbraco should hide top level nodes from generated URLs. /// /// - /// true if umbraco hides top level nodes from urls; otherwise, false. + /// true if umbraco hides top level nodes from URLs; otherwise, false. /// public bool HideTopLevelNodeFromPath { diff --git a/src/Umbraco.Core/Configuration/IGlobalSettings.cs b/src/Umbraco.Core/Configuration/IGlobalSettings.cs index ed71e9344cd5..fd5140042c72 100644 --- a/src/Umbraco.Core/Configuration/IGlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/IGlobalSettings.cs @@ -6,9 +6,9 @@ public interface IGlobalSettings { /// - /// Gets the reserved urls from web.config. + /// Gets the reserved URLs from web.config. /// - /// The reserved urls. + /// The reserved URLs. string ReservedUrls { get; } /// @@ -45,10 +45,10 @@ public interface IGlobalSettings string DefaultUILanguage { get; } /// - /// Gets a value indicating whether umbraco should hide top level nodes from generated urls. + /// Gets a value indicating whether umbraco should hide top level nodes from generated URLs. /// /// - /// true if umbraco hides top level nodes from urls; otherwise, false. + /// true if umbraco hides top level nodes from URLs; otherwise, false. /// bool HideTopLevelNodeFromPath { get; } diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index 8bf49057266a..cb3b0d48d085 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -64,7 +64,7 @@ public static class AppSettings public const string MediaPath = "umbracoMediaPath"; /// - /// The reserved urls from web.config. + /// The reserved URLs from web.config. /// public const string ReservedUrls = "Umbraco.Core.ReservedUrls"; @@ -101,7 +101,7 @@ public static class AppSettings public const string DefaultUILanguage = "Umbraco.Core.DefaultUILanguage"; /// - /// A true/false value indicating whether umbraco should hide top level nodes from generated urls. + /// A true/false value indicating whether umbraco should hide top level nodes from generated URLs. /// public const string HideTopLevelNodeFromPath = "Umbraco.Core.HideTopLevelNodeFromPath"; diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index dcd7eb9d0546..90f5fbd0d0e5 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -192,7 +192,7 @@ public static class Aliases public const string NestedContent = "Umbraco.NestedContent"; /// - /// Alias for the multi url picker editor. + /// Alias for the multi URL picker editor. /// public const string MultiUrlPicker = "Umbraco.MultiUrlPicker"; } diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 43c989805baa..f900288ef54d 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -56,6 +56,8 @@ public static class Security public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; + public const string BackOfficeExternalLoginOptionsProperty = "UmbracoBackOfficeExternalLoginOptions"; + } } } diff --git a/src/Umbraco.Core/GuidUdi.cs b/src/Umbraco.Core/GuidUdi.cs index d78a33a435ec..93f670cd01b4 100644 --- a/src/Umbraco.Core/GuidUdi.cs +++ b/src/Umbraco.Core/GuidUdi.cs @@ -34,7 +34,7 @@ public GuidUdi(Uri uriValue) { Guid guid; if (Guid.TryParse(uriValue.AbsolutePath.TrimStart('/'), out guid) == false) - throw new FormatException("Url \"" + uriValue + "\" is not a guid entity id."); + throw new FormatException("URI \"" + uriValue + "\" is not a GUID entity ID."); Guid = guid; } @@ -48,7 +48,7 @@ public GuidUdi(Uri uriValue) { var udi = Udi.Parse(s); if (udi is GuidUdi == false) - throw new FormatException("String \"" + s + "\" is not a guid entity id."); + throw new FormatException("String \"" + s + "\" is not a GUID entity id."); return (GuidUdi) udi; } diff --git a/src/Umbraco.Core/IO/IFileSystem.cs b/src/Umbraco.Core/IO/IFileSystem.cs index 0405c5925ac9..9de1ea40f258 100644 --- a/src/Umbraco.Core/IO/IFileSystem.cs +++ b/src/Umbraco.Core/IO/IFileSystem.cs @@ -102,7 +102,7 @@ public interface IFileSystem /// /// Returns the application relative path to the file. /// - /// The full path or url. + /// The full path or URL. /// /// The representing the relative path. /// @@ -118,11 +118,11 @@ public interface IFileSystem string GetFullPath(string path); /// - /// Returns the application relative url to the file. + /// Returns the application relative URL to the file. /// - /// The path to return the url for. + /// The path to return the URL for. /// - /// representing the relative url. + /// representing the relative URL. /// string GetUrl(string path); diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index 96aaf7ca2792..5a216e8554d4 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -19,7 +19,7 @@ public class PhysicalFileSystem : IFileSystem // (is used in GetRelativePath) private readonly string _rootPathFwd; - // the relative url, using url separator chars, NOT ending with a separator + // the relative URL, using URL separator chars, NOT ending with a separator // eg "" or "/Views" or "/Media" or "//Media" in case of a virtual path private readonly string _rootUrl; @@ -243,9 +243,9 @@ public bool FileExists(string path) } /// - /// Gets the filesystem-relative path of a full path or of an url. + /// Gets the filesystem-relative path of a full path or of an URL. /// - /// The full path or url. + /// The full path or URL. /// The path, relative to this filesystem's root. /// /// The relative path is relative to this filesystem's root, not starting with any @@ -253,10 +253,10 @@ public bool FileExists(string path) /// public string GetRelativePath(string fullPathOrUrl) { - // test url - var path = fullPathOrUrl.Replace('\\', '/'); // ensure url separator char + // test URL + var path = fullPathOrUrl.Replace('\\', '/'); // ensure URL separator char - // if it starts with the root url, strip it and trim the starting slash to make it relative + // if it starts with the root URL, strip it and trim the starting slash to make it relative // eg "/Media/1234/img.jpg" => "1234/img.jpg" if (IOHelper.PathStartsWith(path, _rootUrl, '/')) return path.Substring(_rootUrl.Length).TrimStart('/'); @@ -319,10 +319,10 @@ public string GetFullPath(string path) } /// - /// Gets the url. + /// Gets the URL. /// /// The filesystem-relative path. - /// The url. + /// The URL. /// All separators are forward-slashes. public string GetUrl(string path) { diff --git a/src/Umbraco.Core/IRuntimeState.cs b/src/Umbraco.Core/IRuntimeState.cs index 30c768ab01ae..9f188b5308e1 100644 --- a/src/Umbraco.Core/IRuntimeState.cs +++ b/src/Umbraco.Core/IRuntimeState.cs @@ -41,7 +41,7 @@ public interface IRuntimeState ServerRole ServerRole { get; } /// - /// Gets the Umbraco application url. + /// Gets the Umbraco application URL. /// /// This is eg "http://www.example.com". Uri ApplicationUrl { get; } diff --git a/src/Umbraco.Core/Media/IEmbedProvider.cs b/src/Umbraco.Core/Media/IEmbedProvider.cs index 99b162e0b7cb..39da6fae0dc5 100644 --- a/src/Umbraco.Core/Media/IEmbedProvider.cs +++ b/src/Umbraco.Core/Media/IEmbedProvider.cs @@ -15,7 +15,7 @@ public interface IEmbedProvider string[] UrlSchemeRegex { get; } /// - /// A collection of querystring request parameters to append to the API Url + /// A collection of querystring request parameters to append to the API URL /// /// ?key=value&key2=value2 Dictionary RequestParams { get; } diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 03ba58d15e20..30f2606bfabd 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Migrations.Upgrade.V_8_0_1; using Umbraco.Core.Migrations.Upgrade.V_8_1_0; using Umbraco.Core.Migrations.Upgrade.V_8_6_0; +using Umbraco.Core.Migrations.Upgrade.V_8_9_0; namespace Umbraco.Core.Migrations.Upgrade { @@ -175,24 +176,27 @@ protected void DefinePlan() To("{78BAF571-90D0-4D28-8175-EF96316DA789}"); // release-8.0.0 - // to 8.0.1... + // to 8.0.1 To("{80C0A0CB-0DD5-4573-B000-C4B7C313C70D}"); // release-8.0.1 - // to 8.1.0... + // to 8.1.0 To("{B69B6E8C-A769-4044-A27E-4A4E18D1645A}"); To("{0372A42B-DECF-498D-B4D1-6379E907EB94}"); To("{5B1E0D93-F5A3-449B-84BA-65366B84E2D4}"); - // to 8.6.0... + // to 8.6.0 To("{4759A294-9860-46BC-99F9-B4C975CAE580}"); To("{0BC866BC-0665-487A-9913-0290BD0169AD}"); To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}"); To("{EE288A91-531B-4995-8179-1D62D9AA3E2E}"); To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}"); - // to 8.7.0... + // to 8.7.0 To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); + + // to 8.9.0 + To("{B5838FF5-1D22-4F6C-BCEB-F83ACB14B575}"); //FINAL } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs new file mode 100644 index 000000000000..45bc1c620be5 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs @@ -0,0 +1,20 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_9_0 +{ + public class ExternalLoginTableUserData : MigrationBase + { + public ExternalLoginTableUserData(IMigrationContext context) + : base(context) + { + } + + /// + /// Adds new column to the External Login table + /// + public override void Migrate() + { + AddColumn(Constants.DatabaseSchema.Tables.ExternalLogin, "userData"); + } + } +} diff --git a/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs index 944000014636..f7daf79ec9b2 100644 --- a/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs @@ -7,7 +7,7 @@ public interface IMediaEntitySlim : IContentEntitySlim { /// - /// The media file's path/url + /// The media file's path/URL /// string MediaPath { get; } } diff --git a/src/Umbraco.Core/Models/IRedirectUrl.cs b/src/Umbraco.Core/Models/IRedirectUrl.cs index e06688164512..527dad57da3f 100644 --- a/src/Umbraco.Core/Models/IRedirectUrl.cs +++ b/src/Umbraco.Core/Models/IRedirectUrl.cs @@ -5,7 +5,7 @@ namespace Umbraco.Core.Models { /// - /// Represents a redirect url. + /// Represents a redirect URL. /// public interface IRedirectUrl : IEntity, IRememberBeingDirty { @@ -22,7 +22,7 @@ public interface IRedirectUrl : IEntity, IRememberBeingDirty Guid ContentKey { get; set; } /// - /// Gets or sets the redirect url creation date. + /// Gets or sets the redirect URL creation date. /// [DataMember] DateTime CreateDateUtc { get; set; } @@ -34,7 +34,7 @@ public interface IRedirectUrl : IEntity, IRememberBeingDirty string Culture { get; set; } /// - /// Gets or sets the redirect url route. + /// Gets or sets the redirect URL route. /// /// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo. [DataMember] diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 8dc056d555e7..b7bde26fae6a 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -275,16 +275,23 @@ public override ICollection Logins { get { - if (_getLogins != null && _getLogins.IsValueCreated == false) + // return if it exists + if (_logins != null) return _logins; + + _logins = new ObservableCollection(); + + // if the callback is there and hasn't been created yet then execute it and populate the logins + if (_getLogins != null && !_getLogins.IsValueCreated) { - _logins = new ObservableCollection(); foreach (var l in _getLogins.Value) { _logins.Add(l); } - //now assign events - _logins.CollectionChanged += Logins_CollectionChanged; } + + //now assign events + _logins.CollectionChanged += Logins_CollectionChanged; + return _logins; } } diff --git a/src/Umbraco.Core/Models/Identity/ExternalLogin.cs b/src/Umbraco.Core/Models/Identity/ExternalLogin.cs new file mode 100644 index 000000000000..6e4abf29061c --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/ExternalLogin.cs @@ -0,0 +1,24 @@ +using System; + +namespace Umbraco.Core.Models.Identity +{ + /// + public class ExternalLogin : IExternalLogin + { + public ExternalLogin(string loginProvider, string providerKey, string userData = null) + { + LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); + ProviderKey = providerKey ?? throw new ArgumentNullException(nameof(providerKey)); + UserData = userData; + } + + /// + public string LoginProvider { get; } + + /// + public string ProviderKey { get; } + + /// + public string UserData { get; } + } +} diff --git a/src/Umbraco.Core/Models/Identity/IExternalLogin.cs b/src/Umbraco.Core/Models/Identity/IExternalLogin.cs new file mode 100644 index 000000000000..68f66a5ceece --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IExternalLogin.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Core.Models.Identity +{ + /// + /// Used to persist external login data for a user + /// + public interface IExternalLogin + { + string LoginProvider { get; } + string ProviderKey { get; } + string UserData { get; } + } +} diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs index 276f60177116..feb8af24f3a9 100644 --- a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs +++ b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs @@ -2,23 +2,30 @@ namespace Umbraco.Core.Models.Identity { + // TODO: Merge these in v8! This is here purely for backward compat + + public interface IIdentityUserLoginExtended : IIdentityUserLogin + { + /// + /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider + /// + string UserData { get; set; } + } + public interface IIdentityUserLogin : IEntity, IRememberBeingDirty { /// /// The login provider for the login (i.e. Facebook, Google) - /// /// string LoginProvider { get; set; } /// /// Key representing the login for the provider - /// /// string ProviderKey { get; set; } /// /// User Id for the user who owns this login - /// /// int UserId { get; set; } } diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs index 5876f420b492..66911b08acd9 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs @@ -3,11 +3,11 @@ namespace Umbraco.Core.Models.Identity { + /// /// Entity type for a user's login (i.e. Facebook, Google) - /// /// - public class IdentityUserLogin : EntityBase, IIdentityUserLogin + public class IdentityUserLogin : EntityBase, IIdentityUserLoginExtended { public IdentityUserLogin(string loginProvider, string providerKey, int userId) { @@ -25,22 +25,16 @@ public IdentityUserLogin(int id, string loginProvider, string providerKey, int u CreateDate = createDate; } - /// - /// The login provider for the login (i.e. Facebook, Google) - /// - /// + /// public string LoginProvider { get; set; } - /// - /// Key representing the login for the provider - /// - /// + /// public string ProviderKey { get; set; } - /// - /// User Id for the user who owns this login - /// - /// + /// public int UserId { get; set; } + + /// + public string UserData { get; set; } } } diff --git a/src/Umbraco.Core/Models/MediaExtensions.cs b/src/Umbraco.Core/Models/MediaExtensions.cs index 96f183b6e63e..c985ace8b0e1 100644 --- a/src/Umbraco.Core/Models/MediaExtensions.cs +++ b/src/Umbraco.Core/Models/MediaExtensions.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Models public static class MediaExtensions { /// - /// Gets the url of a media item. + /// Gets the URL of a media item. /// public static string GetUrl(this IMedia media, string propertyAlias, ILogger logger) { @@ -29,7 +29,7 @@ public static string GetUrl(this IMedia media, string propertyAlias, ILogger log } /// - /// Gets the urls of a media item. + /// Gets the URLs of a media item. /// public static string[] GetUrls(this IMedia media, IContentSection contentSection, ILogger logger) { diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs index 4e154bf5145e..23861dfa9d02 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs @@ -31,7 +31,7 @@ public interface IPublishedContent : IPublishedElement string Name { get; } /// - /// Gets the url segment of the content item for the current culture. + /// Gets the URL segment of the content item for the current culture. /// string UrlSegment { get; } @@ -93,7 +93,7 @@ public interface IPublishedContent : IPublishedElement DateTime UpdateDate { get; } /// - /// Gets the url of the content item for the current culture. + /// Gets the URL of the content item for the current culture. /// /// /// The value of this property is contextual. It depends on the 'current' request uri, diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs index 908b97fc3695..58241ade24ce 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs @@ -32,7 +32,7 @@ public PublishedCultureInfo(string culture, string name, string urlSegment, Date internal string Name { get; } /// - /// Gets the url segment of the item. + /// Gets the URL segment of the item. /// internal string UrlSegment { get; } diff --git a/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs b/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs index 4cd6a680f423..d11459bb9e37 100644 --- a/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs +++ b/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs @@ -1,27 +1,27 @@ namespace Umbraco.Core.Models.PublishedContent { /// - /// Specifies the type of urls that the url provider should produce, Auto is the default. + /// Specifies the type of URLs that the URL provider should produce, Auto is the default. /// public enum UrlMode { /// - /// Indicates that the url provider should do what it has been configured to do. + /// Indicates that the URL provider should do what it has been configured to do. /// Default = 0, /// - /// Indicates that the url provider should produce relative urls exclusively. + /// Indicates that the URL provider should produce relative URLs exclusively. /// Relative, /// - /// Indicates that the url provider should produce absolute urls exclusively. + /// Indicates that the URL provider should produce absolute URLs exclusively. /// Absolute, /// - /// Indicates that the url provider should determine automatically whether to return relative or absolute urls. + /// Indicates that the URL provider should determine automatically whether to return relative or absolute URLs. /// Auto } diff --git a/src/Umbraco.Core/Models/ServerRegistration.cs b/src/Umbraco.Core/Models/ServerRegistration.cs index 7dae5d5393e6..a862b11c23ee 100644 --- a/src/Umbraco.Core/Models/ServerRegistration.cs +++ b/src/Umbraco.Core/Models/ServerRegistration.cs @@ -24,7 +24,7 @@ public ServerRegistration() /// Initializes a new instance of the class. /// /// The unique id of the server registration. - /// The server url. + /// The server URL. /// The unique server identity. /// The date and time the registration was created. /// The date and time the registration was last accessed. @@ -45,7 +45,7 @@ public ServerRegistration(int id, string serverAddress, string serverIdentity, D /// /// Initializes a new instance of the class. /// - /// The server url. + /// The server URL. /// The unique server identity. /// The date and time the registration was created. public ServerRegistration(string serverAddress, string serverIdentity, DateTime registered) @@ -58,7 +58,7 @@ public ServerRegistration(string serverAddress, string serverIdentity, DateTime } /// - /// Gets or sets the server url. + /// Gets or sets the server URL. /// public string ServerAddress { diff --git a/src/Umbraco.Core/Persistence/Dtos/ExternalLoginDto.cs b/src/Umbraco.Core/Persistence/Dtos/ExternalLoginDto.cs index 1b774854a6fd..0a56552000e4 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ExternalLoginDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ExternalLoginDto.cs @@ -14,9 +14,13 @@ internal class ExternalLoginDto [PrimaryKeyColumn(Name = "PK_umbracoExternalLogin")] public int Id { get; set; } + // TODO: This is completely missing a FK!!? + [Column("userId")] public int UserId { get; set; } + // TODO: There should be an index on both LoginProvider and ProviderKey + [Column("loginProvider")] [Length(4000)] [NullSetting(NullSetting = NullSettings.NotNull)] @@ -30,5 +34,13 @@ internal class ExternalLoginDto [Column("createDate")] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime CreateDate { get; set; } + + /// + /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider + /// + [Column("userData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string UserData { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs b/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs index 4309fe511fc9..6c1af68acd00 100644 --- a/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs @@ -1,13 +1,17 @@ -using Umbraco.Core.Models.Identity; +using System; +using Umbraco.Core.Models.Identity; using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Persistence.Factories { internal static class ExternalLoginFactory { - public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto) + public static IIdentityUserLoginExtended BuildEntity(ExternalLoginDto dto) { - var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId, dto.CreateDate); + var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId, dto.CreateDate) + { + UserData = dto.UserData + }; // reset dirty initial properties (U4-1946) entity.ResetDirtyProperties(false); @@ -16,13 +20,30 @@ public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto) public static ExternalLoginDto BuildDto(IIdentityUserLogin entity) { + var asExtended = entity as IIdentityUserLoginExtended; var dto = new ExternalLoginDto { Id = entity.Id, CreateDate = entity.CreateDate, LoginProvider = entity.LoginProvider, ProviderKey = entity.ProviderKey, - UserId = entity.UserId + UserId = entity.UserId, + UserData = asExtended?.UserData + }; + + return dto; + } + + public static ExternalLoginDto BuildDto(int userId, IExternalLogin entity, int? id = null) + { + var dto = new ExternalLoginDto + { + Id = id ?? default, + UserId = userId, + LoginProvider = entity.LoginProvider, + ProviderKey = entity.ProviderKey, + UserData = entity.UserData, + CreateDate = DateTime.Now }; return dto; diff --git a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs index 6d145e9961a2..a5a93449d3cd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Microsoft.AspNet.Identity; using Umbraco.Core.Models.Identity; @@ -6,7 +7,9 @@ namespace Umbraco.Core.Persistence.Repositories { public interface IExternalLoginRepository : IReadWriteQueryRepository { + [Obsolete("Use the overload specifying IIdentityUserLoginExtended instead")] void SaveUserLogins(int memberId, IEnumerable logins); + void Save(int userId, IEnumerable logins); void DeleteUserLogins(int memberId); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs index d05f4e007cf8..a8a88e54dab3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs @@ -10,72 +10,72 @@ namespace Umbraco.Core.Persistence.Repositories public interface IRedirectUrlRepository : IReadWriteQueryRepository { /// - /// Gets a redirect url. + /// Gets a redirect URL. /// - /// The Umbraco redirect url route. + /// The Umbraco redirect URL route. /// The content unique key. /// The culture. /// IRedirectUrl Get(string url, Guid contentKey, string culture); /// - /// Deletes a redirect url. + /// Deletes a redirect URL. /// - /// The redirect url identifier. + /// The redirect URL identifier. void Delete(Guid id); /// - /// Deletes all redirect urls. + /// Deletes all redirect URLs. /// void DeleteAll(); /// - /// Deletes all redirect urls for a given content. + /// Deletes all redirect URLs for a given content. /// /// The content unique key. void DeleteContentUrls(Guid contentKey); /// - /// Gets the most recent redirect url corresponding to an Umbraco redirect url route. + /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. /// - /// The Umbraco redirect url route. - /// The most recent redirect url corresponding to the route. + /// The Umbraco redirect URL route. + /// The most recent redirect URL corresponding to the route. IRedirectUrl GetMostRecentUrl(string url); /// - /// Gets all redirect urls for a content item. + /// Gets all redirect URLs for a content item. /// /// The content unique key. - /// All redirect urls for the content item. + /// All redirect URLs for the content item. IEnumerable GetContentUrls(Guid contentKey); /// - /// Gets all redirect urls. + /// Gets all redirect URLs. /// /// The page index. /// The page size. - /// The total count of redirect urls. - /// The redirect urls. + /// The total count of redirect URLs. + /// The redirect URLs. IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total); /// - /// Gets all redirect urls below a given content item. + /// Gets all redirect URLs below a given content item. /// /// The content unique identifier. /// The page index. /// The page size. - /// The total count of redirect urls. - /// The redirect urls. + /// The total count of redirect URLs. + /// The redirect URLs. IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total); /// - /// Searches for all redirect urls that contain a given search term in their URL property. + /// Searches for all redirect URLs that contain a given search term in their URL property. /// /// The term to search for. /// The page index. /// The page size. - /// The total count of redirect urls. - /// The redirect urls. + /// The total count of redirect URLs. + /// The redirect URLs. IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs index f708590ea845..ad53a2d52234 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -22,25 +22,65 @@ public ExternalLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, IL public void DeleteUserLogins(int memberId) { - Database.Execute("DELETE FROM ExternalLogins WHERE UserId=@userId", new { userId = memberId }); + Database.Delete("WHERE userId=@userId", new { userId = memberId }); } - public void SaveUserLogins(int memberId, IEnumerable logins) + public void Save(int userId, IEnumerable logins) { - //clear out logins for member - Database.Execute("DELETE FROM umbracoExternalLogin WHERE userId=@userId", new { userId = memberId }); - - //add them all - foreach (var l in logins) + var sql = Sql() + .Select() + .From() + .Where(x => x.UserId == userId) + .ForUpdate(); + + // deduplicate the logins + logins = logins.DistinctBy(x => x.ProviderKey + x.LoginProvider).ToList(); + + var toUpdate = new Dictionary(); + var toDelete = new List(); + var toInsert = new List(logins); + + var existingLogins = Database.Query(sql).OrderByDescending(x => x.CreateDate).ToList(); + // used to track duplicates so they can be removed + var keys = new HashSet<(string, string)>(); + + foreach (var existing in existingLogins) { - Database.Insert(new ExternalLoginDto + if (!keys.Add((existing.ProviderKey, existing.LoginProvider))) { - LoginProvider = l.LoginProvider, - ProviderKey = l.ProviderKey, - UserId = memberId, - CreateDate = DateTime.Now - }); + // if it already exists we need to remove this one + toDelete.Add(existing.Id); + } + else + { + var found = logins.FirstOrDefault(x => + x.LoginProvider.Equals(existing.LoginProvider, StringComparison.InvariantCultureIgnoreCase) + && x.ProviderKey.Equals(existing.ProviderKey, StringComparison.InvariantCultureIgnoreCase)); + + if (found != null) + { + toUpdate.Add(existing.Id, found); + // if it's an update then it's not an insert + toInsert.RemoveAll(x => x.ProviderKey == found.ProviderKey && x.LoginProvider == found.LoginProvider); + } + else + { + toDelete.Add(existing.Id); + } + } } + + // do the deletes, updates and inserts + if (toDelete.Count > 0) + Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); + foreach (var u in toUpdate) + Database.Update(ExternalLoginFactory.BuildDto(userId, u.Value, u.Key)); + Database.InsertBulk(toInsert.Select(i => ExternalLoginFactory.BuildDto(userId, i))); + } + + public void SaveUserLogins(int memberId, IEnumerable logins) + { + Save(memberId, logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey))); } protected override IIdentityUserLogin PerformGet(int id) @@ -67,7 +107,7 @@ protected override IEnumerable PerformGetAll(params int[] id return PerformGetAllOnIds(ids); } - var sql = GetBaseQuery(false); + var sql = GetBaseQuery(false).OrderByDescending(x => x.CreateDate); return ConvertFromDtos(Database.Fetch(sql)) .ToArray();// we don't want to re-iterate again! @@ -103,7 +143,7 @@ protected override IEnumerable PerformGetByQuery(IQuery - /// Gets the value image url for a specified crop. + /// Gets the value image URL for a specified crop. /// [Obsolete("Use the overload that takes an IImageUrlGenerator")] public string GetCropUrl(string alias, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null) => GetCropUrl(alias, Current.ImageUrlGenerator, useCropDimensions, useFocalPoint, cacheBusterValue); /// - /// Gets the value image url for a specified crop. + /// Gets the value image URL for a specified crop. /// public string GetCropUrl(string alias, IImageUrlGenerator imageUrlGenerator, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null) { @@ -110,13 +110,13 @@ public string GetCropUrl(string alias, IImageUrlGenerator imageUrlGenerator, boo } /// - /// Gets the value image url for a specific width and height. + /// Gets the value image URL for a specific width and height. /// [Obsolete("Use the overload that takes an IImageUrlGenerator")] public string GetCropUrl(int width, int height, bool useFocalPoint = false, string cacheBusterValue = null) => GetCropUrl(width, height, Current.ImageUrlGenerator, useFocalPoint, cacheBusterValue); /// - /// Gets the value image url for a specific width and height. + /// Gets the value image URL for a specific width and height. /// public string GetCropUrl(int width, int height, IImageUrlGenerator imageUrlGenerator, bool useFocalPoint = false, string cacheBusterValue = null) { diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs index 36ffddb8634a..8926174c035e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs @@ -42,7 +42,7 @@ public override object ConvertSourceToIntermediate(IPublishedElement owner, IPub } catch (Exception ex) { - // cannot deserialize, assume it may be a raw image url + // cannot deserialize, assume it may be a raw image URL Current.Logger.Error(ex, "Could not deserialize string '{JsonString}' into an image cropper value.", sourceString); value = new ImageCropperValue { Src = sourceString }; } diff --git a/src/Umbraco.Core/PublishedContentExtensions.cs b/src/Umbraco.Core/PublishedContentExtensions.cs index 921883b822fc..4aa3b77a7b4b 100644 --- a/src/Umbraco.Core/PublishedContentExtensions.cs +++ b/src/Umbraco.Core/PublishedContentExtensions.cs @@ -61,10 +61,10 @@ public static string Name(this IPublishedContent content, string culture = null) /// - /// Gets the url segment of the content item. + /// Gets the URL segment of the content item. /// /// The content item. - /// The specific culture to get the url segment for. If null is used the current culture is used (Default is null). + /// The specific culture to get the URL segment for. If null is used the current culture is used (Default is null). public static string UrlSegment(this IPublishedContent content, string culture = null) { // invariant has invariant value (whatever the requested culture) @@ -104,7 +104,7 @@ public static DateTime CultureDate(this IPublishedContent content, string cultur /// /// The content item. /// - /// The specific culture to get the url children for. Default is null which will use the current culture in + /// The specific culture to get the URL children for. Default is null which will use the current culture in /// /// /// Gets children that are available for the specified culture. diff --git a/src/Umbraco.Core/RuntimeState.cs b/src/Umbraco.Core/RuntimeState.cs index 74ec70828be9..8a30a97e7b28 100644 --- a/src/Umbraco.Core/RuntimeState.cs +++ b/src/Umbraco.Core/RuntimeState.cs @@ -102,7 +102,7 @@ internal void EnsureApplicationUrl(HttpRequestBase request = null) // about this is that this is here specifically for the slot swap scenario https://issues.umbraco.org/issue/U4-10626 - // see U4-10626 - in some cases we want to reset the application url + // see U4-10626 - in some cases we want to reset the application URL // (this is a simplified version of what was in 7.x) // note: should this be optional? is it expensive? var url = request == null ? null : ApplicationUrlHelper.GetApplicationUrlFromCurrentRequest(request, _globalSettings); diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index dc271452e10d..7df328b5b7ba 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -96,9 +96,10 @@ public Task CreateAsync(BackOfficeIdentityUser user) IsLockedOut = user.IsLockedOut, }; - UpdateMemberProperties(userEntity, user); + // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. + var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins)); - // TODO: We should deal with Roles --> User Groups here which we currently are not doing + UpdateMemberProperties(userEntity, user); _userService.Save(userEntity); @@ -107,6 +108,16 @@ public Task CreateAsync(BackOfficeIdentityUser user) //re-assign id user.Id = userEntity.Id; + if (isLoginsPropertyDirty) + { + _externalLoginService.Save( + user.Id, + user.Logins.Select(x => new ExternalLogin( + x.LoginProvider, + x.ProviderKey, + (x is IIdentityUserLoginExtended extended) ? extended.UserData : null))); + } + return Task.FromResult(0); } @@ -115,7 +126,7 @@ public Task CreateAsync(BackOfficeIdentityUser user) /// /// /// - public async Task UpdateAsync(BackOfficeIdentityUser user) + public Task UpdateAsync(BackOfficeIdentityUser user) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); @@ -126,11 +137,13 @@ public async Task UpdateAsync(BackOfficeIdentityUser user) throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); } + // TODO: Wrap this in a scope! + var found = _userService.GetUserById(asInt.Result); if (found != null) { // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. - var isLoginsPropertyDirty = user.IsPropertyDirty("Logins"); + var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins)); if (UpdateMemberProperties(found, user)) { @@ -139,10 +152,16 @@ public async Task UpdateAsync(BackOfficeIdentityUser user) if (isLoginsPropertyDirty) { - var logins = await GetLoginsAsync(user); - _externalLoginService.SaveUserLogins(found.Id, logins); + _externalLoginService.Save( + found.Id, + user.Logins.Select(x => new ExternalLogin( + x.LoginProvider, + x.ProviderKey, + (x is IIdentityUserLoginExtended extended) ? extended.UserData : null))); } } + + return Task.CompletedTask; } /// @@ -382,7 +401,7 @@ public Task FindAsync(UserLoginInfo login) if (login == null) throw new ArgumentNullException(nameof(login)); //get all logins associated with the login id - var result = _externalLoginService.Find(login).ToArray(); + var result = _externalLoginService.Find(login.LoginProvider, login.ProviderKey).ToArray(); if (result.Any()) { //return the first user that matches the result @@ -633,7 +652,7 @@ private bool UpdateMemberProperties(IUser user, BackOfficeIdentityUser identityU //don't assign anything if nothing has changed as this will trigger the track changes of the model - if (identityUser.IsPropertyDirty("LastLoginDateUtc") + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastLoginDateUtc)) || (user.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false) || identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value) { @@ -642,33 +661,33 @@ private bool UpdateMemberProperties(IUser user, BackOfficeIdentityUser identityU var dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime(); user.LastLoginDate = dt; } - if (identityUser.IsPropertyDirty("LastPasswordChangeDateUtc") + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastPasswordChangeDateUtc)) || (user.LastPasswordChangeDate != default(DateTime) && identityUser.LastPasswordChangeDateUtc.HasValue == false) || identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value) { anythingChanged = true; user.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime(); } - if (identityUser.IsPropertyDirty("EmailConfirmed") + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.EmailConfirmed)) || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default(DateTime) && identityUser.EmailConfirmed == false) || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default(DateTime)) && identityUser.EmailConfirmed)) { anythingChanged = true; user.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null; } - if (identityUser.IsPropertyDirty("Name") + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Name)) && user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Name = identityUser.Name; } - if (identityUser.IsPropertyDirty("Email") + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Email)) && user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Email = identityUser.Email; } - if (identityUser.IsPropertyDirty("AccessFailedCount") + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.AccessFailedCount)) && user.FailedPasswordAttempts != identityUser.AccessFailedCount) { anythingChanged = true; @@ -686,32 +705,32 @@ private bool UpdateMemberProperties(IUser user, BackOfficeIdentityUser identityU } } - if (identityUser.IsPropertyDirty("UserName") + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.UserName)) && user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Username = identityUser.UserName; } - if (identityUser.IsPropertyDirty("PasswordHash") + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.PasswordHash)) && user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.RawPasswordValue = identityUser.PasswordHash; } - if (identityUser.IsPropertyDirty("Culture") + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Culture)) && user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Language = identityUser.Culture; } - if (identityUser.IsPropertyDirty("StartMediaIds") + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.StartMediaIds)) && user.StartMediaIds.UnsortedSequenceEqual(identityUser.StartMediaIds) == false) { anythingChanged = true; user.StartMediaIds = identityUser.StartMediaIds; } - if (identityUser.IsPropertyDirty("StartContentIds") + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.StartContentIds)) && user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds) == false) { anythingChanged = true; @@ -724,7 +743,7 @@ private bool UpdateMemberProperties(IUser user, BackOfficeIdentityUser identityU } // TODO: Fix this for Groups too - if (identityUser.IsPropertyDirty("Roles") || identityUser.IsPropertyDirty("Groups")) + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups))) { var userGroupAliases = user.Groups.Select(x => x.Alias).ToArray(); diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs index 633e12bcc1e9..0bc8de492acb 100644 --- a/src/Umbraco.Core/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs @@ -31,7 +31,9 @@ public string HashPasswordForStorage(string password) public bool VerifyPassword(string password, string hashedPassword) { - if (string.IsNullOrWhiteSpace(hashedPassword)) throw new ArgumentException("Value cannot be null or whitespace.", "hashedPassword"); + if (string.IsNullOrWhiteSpace(hashedPassword)) + return false; + return CheckPassword(password, hashedPassword); } diff --git a/src/Umbraco.Core/Services/IExternalLoginService.cs b/src/Umbraco.Core/Services/IExternalLoginService.cs index a81543cf2d79..2749ca7b8bac 100644 --- a/src/Umbraco.Core/Services/IExternalLoginService.cs +++ b/src/Umbraco.Core/Services/IExternalLoginService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Microsoft.AspNet.Identity; using Umbraco.Core.Models.Identity; @@ -16,20 +17,38 @@ public interface IExternalLoginService : IService /// IEnumerable GetAll(int userId); + [Obsolete("Use the overload specifying loginProvider and providerKey instead")] + IEnumerable Find(UserLoginInfo login); + /// /// Returns all logins matching the login info - generally there should only be one but in some cases /// there might be more than one depending on if an administrator has been editing/removing members /// - /// + /// + /// /// - IEnumerable Find(UserLoginInfo login); + IEnumerable Find(string loginProvider, string providerKey); + + [Obsolete("Use the Save method instead")] + void SaveUserLogins(int userId, IEnumerable logins); /// - /// Save user logins + /// Saves the external logins associated with the user /// - /// + /// + /// The user associated with the logins + /// /// - void SaveUserLogins(int userId, IEnumerable logins); + /// + /// This will replace all external login provider information for the user + /// + void Save(int userId, IEnumerable logins); + + /// + /// Save a single external login record + /// + /// + void Save(IIdentityUserLoginExtended login); /// /// Deletes all user logins - normally used when a member is deleted diff --git a/src/Umbraco.Core/Services/IIconService.cs b/src/Umbraco.Core/Services/IIconService.cs index 963edb22a593..72174a05047c 100644 --- a/src/Umbraco.Core/Services/IIconService.cs +++ b/src/Umbraco.Core/Services/IIconService.cs @@ -9,13 +9,13 @@ public interface IIconService /// Gets an IconModel containing the icon name and SvgString according to an icon name found at the global icons path /// /// - /// + /// IconModel GetIcon(string iconName); /// /// Gets a list of all svg icons found at at the global icons path. /// - /// + /// A list of IList GetAllIcons(); } } diff --git a/src/Umbraco.Core/Services/IRedirectUrlService.cs b/src/Umbraco.Core/Services/IRedirectUrlService.cs index 3bd9b6f2cf5e..d509dda1a850 100644 --- a/src/Umbraco.Core/Services/IRedirectUrlService.cs +++ b/src/Umbraco.Core/Services/IRedirectUrlService.cs @@ -10,78 +10,78 @@ namespace Umbraco.Core.Services public interface IRedirectUrlService : IService { /// - /// Registers a redirect url. + /// Registers a redirect URL. /// - /// The Umbraco url route. + /// The Umbraco URL route. /// The content unique key. /// The culture. /// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo. void Register(string url, Guid contentKey, string culture = null); /// - /// Deletes all redirect urls for a given content. + /// Deletes all redirect URLs for a given content. /// /// The content unique key. void DeleteContentRedirectUrls(Guid contentKey); /// - /// Deletes a redirect url. + /// Deletes a redirect URL. /// - /// The redirect url to delete. + /// The redirect URL to delete. void Delete(IRedirectUrl redirectUrl); /// - /// Deletes a redirect url. + /// Deletes a redirect URL. /// - /// The redirect url identifier. + /// The redirect URL identifier. void Delete(Guid id); /// - /// Deletes all redirect urls. + /// Deletes all redirect URLs. /// void DeleteAll(); /// - /// Gets the most recent redirect urls corresponding to an Umbraco redirect url route. + /// Gets the most recent redirect URLs corresponding to an Umbraco redirect URL route. /// - /// The Umbraco redirect url route. - /// The most recent redirect urls corresponding to the route. + /// The Umbraco redirect URL route. + /// The most recent redirect URLs corresponding to the route. IRedirectUrl GetMostRecentRedirectUrl(string url); /// - /// Gets all redirect urls for a content item. + /// Gets all redirect URLs for a content item. /// /// The content unique key. - /// All redirect urls for the content item. + /// All redirect URLs for the content item. IEnumerable GetContentRedirectUrls(Guid contentKey); /// - /// Gets all redirect urls. + /// Gets all redirect URLs. /// /// The page index. /// The page size. - /// The total count of redirect urls. - /// The redirect urls. + /// The total count of redirect URLs. + /// The redirect URLs. IEnumerable GetAllRedirectUrls(long pageIndex, int pageSize, out long total); /// - /// Gets all redirect urls below a given content item. + /// Gets all redirect URLs below a given content item. /// /// The content unique identifier. /// The page index. /// The page size. - /// The total count of redirect urls. - /// The redirect urls. + /// The total count of redirect URLs. + /// The redirect URLs. IEnumerable GetAllRedirectUrls(int rootContentId, long pageIndex, int pageSize, out long total); /// - /// Searches for all redirect urls that contain a given search term in their URL property. + /// Searches for all redirect URLs that contain a given search term in their URL property. /// /// The term to search for. /// The page index. /// The page size. - /// The total count of redirect urls. - /// The redirect urls. + /// The total count of redirect URLs. + /// The redirect URLs. IEnumerable SearchRedirectUrls(string searchTerm, long pageIndex, int pageSize, out long total); } } diff --git a/src/Umbraco.Core/Services/IServerRegistrationService.cs b/src/Umbraco.Core/Services/IServerRegistrationService.cs index 47bf3838f215..62bb68eb145a 100644 --- a/src/Umbraco.Core/Services/IServerRegistrationService.cs +++ b/src/Umbraco.Core/Services/IServerRegistrationService.cs @@ -10,7 +10,7 @@ public interface IServerRegistrationService /// /// Touches a server to mark it as active; deactivate stale servers. /// - /// The server url. + /// The server URL. /// The server unique identity. /// The time after which a server is considered stale. void TouchServer(string serverAddress, string serverIdentity, TimeSpan staleTimeout); diff --git a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs index 5189b3422e27..d751a82dfb01 100644 --- a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs @@ -177,7 +177,7 @@ public XElement Serialize(IDataType dataType) var folderNames = string.Empty; if (dataType.Level != 1) { - //get url encoded folder names + //get URL encoded folder names var folders = _dataTypeService.GetContainers(dataType) .OrderBy(x => x.Level) .Select(x => HttpUtility.UrlEncode(x.Name)); @@ -518,7 +518,7 @@ public XElement Serialize(IContentType contentType) //don't add folders if this is a child doc type if (contentType.Level != 1 && masterContentType == null) { - //get url encoded folder names + //get URL encoded folder names var folders = _contentTypeService.GetContainers(contentType) .OrderBy(x => x.Level) .Select(x => HttpUtility.UrlEncode(x.Name)); diff --git a/src/Umbraco.Core/Services/Implement/ExternalLoginService.cs b/src/Umbraco.Core/Services/Implement/ExternalLoginService.cs index aedf3874dd77..b0d9a80a6500 100644 --- a/src/Umbraco.Core/Services/Implement/ExternalLoginService.cs +++ b/src/Umbraco.Core/Services/Implement/ExternalLoginService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.Identity; using Umbraco.Core.Events; @@ -20,53 +21,60 @@ public ExternalLoginService(IScopeProvider provider, ILogger logger, IEventMessa _externalLoginRepository = externalLoginRepository; } - /// - /// Returns all user logins assigned - /// - /// - /// + /// public IEnumerable GetAll(int userId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _externalLoginRepository.Get(Query().Where(x => x.UserId == userId)) - .ToList(); // ToList is important here, must evaluate within uow! // ToList is important here, must evaluate within uow! + .ToList(); } } - /// - /// Returns all logins matching the login info - generally there should only be one but in some cases - /// there might be more than one depending on if an administrator has been editing/removing members - /// - /// - /// + [Obsolete("Use the overload specifying loginProvider and providerKey instead")] public IEnumerable Find(UserLoginInfo login) + { + return Find(login.LoginProvider, login.ProviderKey); + } + + /// + public IEnumerable Find(string loginProvider, string providerKey) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - return _externalLoginRepository.Get(Query().Where(x => x.ProviderKey == login.ProviderKey && x.LoginProvider == login.LoginProvider)) - .ToList(); // ToList is important here, must evaluate within uow! // ToList is important here, must evaluate within uow! + return _externalLoginRepository.Get(Query() + .Where(x => x.ProviderKey == providerKey && x.LoginProvider == loginProvider)) + .ToList(); } } - /// - /// Save user logins - /// - /// - /// + [Obsolete("Use the Save method instead")] public void SaveUserLogins(int userId, IEnumerable logins) + { + Save(userId, logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey))); + } + + /// + public void Save(int userId, IEnumerable logins) + { + using (var scope = ScopeProvider.CreateScope()) + { + _externalLoginRepository.Save(userId, logins); + scope.Complete(); + } + } + + /// + public void Save(IIdentityUserLoginExtended login) { using (var scope = ScopeProvider.CreateScope()) { - _externalLoginRepository.SaveUserLogins(userId, logins); + _externalLoginRepository.Save(login); scope.Complete(); } } - /// - /// Deletes all user logins - normally used when a member is deleted - /// - /// + /// public void DeleteUserLogins(int userId) { using (var scope = ScopeProvider.CreateScope()) @@ -75,5 +83,7 @@ public void DeleteUserLogins(int userId) scope.Complete(); } } + + } } diff --git a/src/Umbraco.Core/Services/Implement/NotificationService.cs b/src/Umbraco.Core/Services/Implement/NotificationService.cs index 2b21945ba855..507da9cec3d3 100644 --- a/src/Umbraco.Core/Services/Implement/NotificationService.cs +++ b/src/Umbraco.Core/Services/Implement/NotificationService.cs @@ -394,7 +394,7 @@ private NotificationRequest CreateNotificationRequest(IUser performingUser, IUse content.Id.ToString(CultureInfo.InvariantCulture), string.Format("{2}://{0}/{1}", string.Concat(siteUri.Authority), - // TODO: RE-enable this so we can have a nice url + // TODO: RE-enable this so we can have a nice URL /*umbraco.library.NiceUrl(documentObject.Id))*/ string.Concat(content.Id, ".aspx"), protocol), diff --git a/src/Umbraco.Core/Services/Implement/ServerRegistrationService.cs b/src/Umbraco.Core/Services/Implement/ServerRegistrationService.cs index d9ce978274b3..056d4d9fd9ba 100644 --- a/src/Umbraco.Core/Services/Implement/ServerRegistrationService.cs +++ b/src/Umbraco.Core/Services/Implement/ServerRegistrationService.cs @@ -40,7 +40,7 @@ public ServerRegistrationService(IScopeProvider scopeProvider, ILogger logger, I /// /// Touches a server to mark it as active; deactivate stale servers. /// - /// The server url. + /// The server URL. /// The server unique identity. /// The time after which a server is considered stale. public void TouchServer(string serverAddress, string serverIdentity, TimeSpan staleTimeout) diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index c37f8bdf353d..57ef9c0d42ff 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -106,7 +106,7 @@ public static string GetFileExtension(this string file) /// internal static Attempt DetectIsJavaScriptPath(this string input) { - //validate that this is a url, if it is not, we'll assume that it is a text block and render it as a text + //validate that this is a URL, if it is not, we'll assume that it is a text block and render it as a text //block instead. var isValid = true; @@ -114,7 +114,7 @@ internal static Attempt DetectIsJavaScriptPath(this string input) { //ok it validates, but so does alert('hello'); ! so we need to do more checks - //here are the valid chars in a url without escaping + //here are the valid chars in a URL without escaping if (Regex.IsMatch(input, @"[^a-zA-Z0-9-._~:/?#\[\]@!$&'\(\)*\+,%;=]")) isValid = false; @@ -856,7 +856,7 @@ internal static string UrlTokenEncode(byte[] input) var pos = str.IndexOf('='); if (pos < 0) pos = str.Length; - // replace chars that would cause problems in urls + // replace chars that would cause problems in URLs var chArray = new char[pos]; for (var i = 0; i < pos; i++) { @@ -1095,13 +1095,13 @@ public static string ToSafeAlias(this string alias, string culture) return Current.ShortStringHelper.CleanStringForSafeAlias(alias, culture); } - // the new methods to get a url segment + // the new methods to get a URL segment /// - /// Cleans a string to produce a string that can safely be used in an url segment. + /// Cleans a string to produce a string that can safely be used in an URL segment. /// /// The text to filter. - /// The safe url segment. + /// The safe URL segment. public static string ToUrlSegment(this string text) { if (text == null) throw new ArgumentNullException(nameof(text)); @@ -1111,11 +1111,11 @@ public static string ToUrlSegment(this string text) } /// - /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an url segment. + /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an URL segment. /// /// The text to filter. /// The culture. - /// The safe url segment. + /// The safe URL segment. public static string ToUrlSegment(this string text, string culture) { if (text == null) throw new ArgumentNullException(nameof(text)); @@ -1124,7 +1124,7 @@ public static string ToUrlSegment(this string text, string culture) return Current.ShortStringHelper.CleanStringForUrlSegment(text, culture); } - // the new methods to clean a string (to alias, url segment...) + // the new methods to clean a string (to alias, URL segment...) /// /// Cleans a string. @@ -1205,7 +1205,7 @@ internal static string SpaceCamelCasing(this string phrase) /// /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a url). + /// both internally (on disk) and externally (as a URL). /// /// The text to filter. /// The safe filename. @@ -1216,7 +1216,7 @@ public static string ToSafeFileName(this string text) /// /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a url). + /// both internally (on disk) and externally (as a URL). /// /// The text to filter. /// The culture. diff --git a/src/Umbraco.Core/Strings/ContentBaseExtensions.cs b/src/Umbraco.Core/Strings/ContentBaseExtensions.cs index 43e3506a84ab..01560f3b8a22 100644 --- a/src/Umbraco.Core/Strings/ContentBaseExtensions.cs +++ b/src/Umbraco.Core/Strings/ContentBaseExtensions.cs @@ -6,17 +6,17 @@ namespace Umbraco.Core.Strings { /// - /// Provides extension methods to IContentBase to get url segments. + /// Provides extension methods to IContentBase to get URL segments. /// internal static class ContentBaseExtensions { /// - /// Gets the url segment for a specified content and culture. + /// Gets the URL segment for a specified content and culture. /// /// The content. /// The culture. /// - /// The url segment. + /// The URL segment. public static string GetUrlSegment(this IContentBase content, IEnumerable urlSegmentProviders, string culture = null) { if (content == null) throw new ArgumentNullException(nameof(content)); diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs index 636118660494..15a87717d230 100644 --- a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs +++ b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.Strings { /// - /// New default implementation of string functions for short strings such as aliases or url segments. + /// New default implementation of string functions for short strings such as aliases or URL segments. /// /// /// Not optimized to work on large bodies of text. @@ -92,10 +92,10 @@ public virtual string CleanStringForSafeAlias(string text, string culture) } /// - /// Cleans a string to produce a string that can safely be used in an url segment. + /// Cleans a string to produce a string that can safely be used in an URL segment. /// /// The text to filter. - /// The safe url segment. + /// The safe URL segment. /// /// The string will be cleaned in the context of the default culture. /// Url segments are Ascii only (no accents...). @@ -106,11 +106,11 @@ public virtual string CleanStringForUrlSegment(string text) } /// - /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an url segment. + /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an URL segment. /// /// The text to filter. /// The culture. - /// The safe url segment. + /// The safe URL segment. /// /// Url segments are Ascii only (no accents...). /// @@ -121,7 +121,7 @@ public virtual string CleanStringForUrlSegment(string text, string culture) /// /// Cleans a string, in the context of the default culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a url). + /// both internally (on disk) and externally (as a URL). /// /// The text to filter. /// The safe filename. @@ -133,7 +133,7 @@ public virtual string CleanStringForSafeFileName(string text) /// /// Cleans a string to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a url). + /// both internally (on disk) and externally (as a URL). /// /// The text to filter. /// The culture. diff --git a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs index 9472ff4823fa..a0f8c735486e 100644 --- a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs @@ -8,11 +8,11 @@ namespace Umbraco.Core.Strings public class DefaultUrlSegmentProvider : IUrlSegmentProvider { /// - /// Gets the url segment for a specified content and culture. + /// Gets the URL segment for a specified content and culture. /// /// The content. /// The culture. - /// The url segment. + /// The URL segment. public string GetUrlSegment(IContentBase content, string culture = null) { return GetUrlSegmentSource(content, culture).ToUrlSegment(culture); diff --git a/src/Umbraco.Core/Strings/IShortStringHelper.cs b/src/Umbraco.Core/Strings/IShortStringHelper.cs index fecbeaaee96b..37873caded08 100644 --- a/src/Umbraco.Core/Strings/IShortStringHelper.cs +++ b/src/Umbraco.Core/Strings/IShortStringHelper.cs @@ -1,7 +1,7 @@ namespace Umbraco.Core.Strings { /// - /// Provides string functions for short strings such as aliases or url segments. + /// Provides string functions for short strings such as aliases or URL segments. /// /// Not necessarily optimized to work on large bodies of text. public interface IShortStringHelper @@ -26,24 +26,24 @@ public interface IShortStringHelper string CleanStringForSafeAlias(string text, string culture); /// - /// Cleans a string to produce a string that can safely be used in an url segment. + /// Cleans a string to produce a string that can safely be used in an URL segment. /// /// The text to filter. - /// The safe url segment. + /// The safe URL segment. /// The string will be cleaned in the context of the IShortStringHelper default culture. string CleanStringForUrlSegment(string text); /// - /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an url segment. + /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an URL segment. /// /// The text to filter. /// The culture. - /// The safe url segment. + /// The safe URL segment. string CleanStringForUrlSegment(string text, string culture); /// /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a url). + /// both internally (on disk) and externally (as a URL). /// /// The text to filter. /// The safe filename. @@ -52,7 +52,7 @@ public interface IShortStringHelper /// /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a url). + /// both internally (on disk) and externally (as a URL). /// /// The text to filter. /// The culture. diff --git a/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs index 12d2ef9a17f0..fddb87e71611 100644 --- a/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs @@ -4,20 +4,20 @@ namespace Umbraco.Core.Strings { /// - /// Provides url segments for content. + /// Provides URL segments for content. /// /// Url segments should comply with IETF RFCs regarding content, encoding, etc. public interface IUrlSegmentProvider { /// - /// Gets the url segment for a specified content and culture. + /// Gets the URL segment for a specified content and culture. /// /// The content. /// The culture. - /// The url segment. - /// This is for when Umbraco is capable of managing more than one url + /// The URL segment. + /// This is for when Umbraco is capable of managing more than one URL /// per content, in 1-to-1 multilingual configurations. Then there would be one - /// url per culture. + /// URL per culture. string GetUrlSegment(IContentBase content, string culture = null); // TODO: For the 301 tracking, we need to add another extended interface to this so that diff --git a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs index 2a558f85aac2..95b08c8377fd 100644 --- a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs +++ b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Sync { /// - /// A helper used to determine the current server umbraco application url. + /// A helper used to determine the current server umbraco application URL. /// public static class ApplicationUrlHelper { @@ -17,12 +17,12 @@ public static class ApplicationUrlHelper private static readonly Type TypeOfApplicationUrlHelper = typeof(ApplicationUrlHelper); /// - /// Gets or sets a custom provider for the umbraco application url. + /// Gets or sets a custom provider for the umbraco application URL. /// /// /// Receives the current request as a parameter, and it may be null. Must return a properly - /// formatted url with scheme and umbraco dir and no trailing slash eg "http://www.mysite.com/umbraco", - /// or null. To be used in auto-load-balancing scenarios where the application url is not + /// formatted URL with scheme and umbraco dir and no trailing slash eg "http://www.mysite.com/umbraco", + /// or null. To be used in auto-load-balancing scenarios where the application URL is not /// in config files but is determined programmatically. /// Must be assigned before resolution is frozen. /// @@ -67,7 +67,7 @@ internal static string TryGetApplicationUrl(IUmbracoSettingsSection settings, IL } // try the server registrar - // which is assumed to return a url that: + // which is assumed to return a URL that: // - end with SystemDirectories.Umbraco // - contain a scheme // - end or not with a slash, it will be taken care of diff --git a/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs b/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs index 276ce6097c6e..a4315c33e991 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs @@ -46,11 +46,11 @@ public ServerRole GetCurrentServerRole() } /// - /// Gets the current umbraco application url. + /// Gets the current umbraco application URL. /// public string GetCurrentServerUmbracoApplicationUrl() { - // this registrar does not provide the umbraco application url + // this registrar does not provide the umbraco application URL return null; } diff --git a/src/Umbraco.Core/Sync/IServerRegistrar.cs b/src/Umbraco.Core/Sync/IServerRegistrar.cs index a4edb1d0e903..8bc8021eec82 100644 --- a/src/Umbraco.Core/Sync/IServerRegistrar.cs +++ b/src/Umbraco.Core/Sync/IServerRegistrar.cs @@ -18,11 +18,11 @@ public interface IServerRegistrar ServerRole GetCurrentServerRole(); /// - /// Gets the current umbraco application url. + /// Gets the current umbraco application URL. /// /// - /// If the registrar does not provide the umbraco application url, should return null. - /// Must return null, or a url that ends with SystemDirectories.Umbraco, and contains a scheme, eg "http://www.mysite.com/umbraco". + /// If the registrar does not provide the umbraco application URL, should return null. + /// Must return null, or a URL that ends with SystemDirectories.Umbraco, and contains a scheme, eg "http://www.mysite.com/umbraco". /// string GetCurrentServerUmbracoApplicationUrl(); } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index b7b9867618cd..04ae182b46c6 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -132,6 +132,7 @@ + @@ -143,6 +144,8 @@ + + diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs index 15ca2cca2476..4051bc3bd551 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Umbraco.Core; @@ -25,6 +26,13 @@ protected override IEnumerable Validate(TModel model) //don't do anything if we're not enabled if (!_config.Enable) yield break; + //list of reserved/disallowed aliases for content/media/member types - more can be added as the need arises + var reservedModelAliases = new[] { "system" }; + if(reservedModelAliases.Contains(model.Alias, StringComparer.OrdinalIgnoreCase)) + { + yield return new ValidationResult($"The model alias {model.Alias} is a reserved term and cannot be used", new[] { "Alias" }); + } + var properties = model.Groups.SelectMany(x => x.Properties) .Where(x => x.Inherited == false) .ToArray(); diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs index c599785711ca..826b3925927b 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs @@ -8,7 +8,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose /// /// Special component used for when MB is disabled with the legacy MB is detected /// - internal class DisabledModelsBuilderComponent : IComponent + public sealed class DisabledModelsBuilderComponent : IComponent { private readonly UmbracoFeatures _features; diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs index 7434131dc2b4..a475e4e1c354 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs @@ -58,7 +58,7 @@ public void Terminate() private void InstallServerVars() { - // register our url - for the backoffice api + // register our URL - for the backoffice API ServerVariablesParser.Parsing += ServerVariablesParser_Parsing; } diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs index 02949d5345b7..f55ced0dbc1b 100644 --- a/src/Umbraco.TestData/UmbracoTestDataController.cs +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -172,7 +172,7 @@ private IEnumerable CreateMediaTree(string company, Faker faker, int count, { var imageUrl = faker.Image.PicsumUrl(); - // we are appending a &ext=.jpg to the end of this for a reason. The result of this url will be something like: + // we are appending a &ext=.jpg to the end of this for a reason. The result of this URL will be something like: // https://picsum.photos/640/480/?image=106 // and due to the way that we detect images there must be an extension so we'll change it to // https://picsum.photos/640/480/?image=106&ext=.jpg diff --git a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs index 343994b03a69..43a120d1544f 100644 --- a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs +++ b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs @@ -887,12 +887,12 @@ public void ShadowGetRelativePath() } /// - /// Ensure the url returned contains the path relative to the FS root, + /// Ensure the URL returned contains the path relative to the FS root, /// but including the rootUrl the FS was initialized with. /// /// /// This file stuff in this test is kinda irrelevant with the current implementation. - /// We do tests that the files are written to the correct places and the url is returned correct, + /// We do tests that the files are written to the correct places and the URL is returned correct, /// but GetUrl is currently really just string manipulation so files are not actually hit by the code. /// Leaving the file stuff in here for now in case the method becomes more clever at some point. /// diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs index 48f0e7b27ef8..893a170f5a67 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs @@ -96,7 +96,7 @@ private void AddToCacheIfDeepestRoute(IPublishedContent content, string route) // so we have a route that maps to a content... say "1234/path/to/content" - however, there could be a // domain set on "to" and route "4567/content" would also map to the same content - and due to how - // urls computing work (by walking the tree up to the first domain we find) it is that second route + // URLs computing work (by walking the tree up to the first domain we find) it is that second route // that would be returned - the "deepest" route - and that is the route we want to cache, *not* the // longer one - so make sure we don't cache the wrong route @@ -257,7 +257,7 @@ string DetermineRouteById(bool preview, int contentId) if (node == null) return null; // walk up from that node until we hit a node with a domain, - // or we reach the content root, collecting urls in the way + // or we reach the content root, collecting URLs in the way var pathParts = new List(); var n = node; var hasDomains = _domainCache.HasAssigned(n.Id); diff --git a/src/Umbraco.Tests/Misc/ApplicationUrlHelperTests.cs b/src/Umbraco.Tests/Misc/ApplicationUrlHelperTests.cs index 79878a59a146..163effb676ba 100644 --- a/src/Umbraco.Tests/Misc/ApplicationUrlHelperTests.cs +++ b/src/Umbraco.Tests/Misc/ApplicationUrlHelperTests.cs @@ -77,7 +77,7 @@ public void SetApplicationUrlViaProvider() [Test] public void SetApplicationUrlWhenNoSettings() { - // no applicable settings, cannot set url + // no applicable settings, cannot set URL var settings = Mock.Of(section => section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string) null)); diff --git a/src/Umbraco.Tests/Routing/ContentFinderByUrlTests.cs b/src/Umbraco.Tests/Routing/ContentFinderByUrlTests.cs index 85168e449072..18bb6bdfb5cb 100644 --- a/src/Umbraco.Tests/Routing/ContentFinderByUrlTests.cs +++ b/src/Umbraco.Tests/Routing/ContentFinderByUrlTests.cs @@ -24,7 +24,7 @@ public class ContentFinderByUrlTests : BaseWebTest [TestCase("/home/sub1", -1)] // should fail // these two are special. getNiceUrl(1046) returns "/" but getNiceUrl(1172) cannot also return "/" so - // we've made it return "/test-page" => we have to support that url back in the lookup... + // we've made it return "/test-page" => we have to support that URL back in the lookup... [TestCase("/home", 1046)] [TestCase("/test-page", 1172)] public void Match_Document_By_Url_Hide_Top_Level(string urlString, int expectedId) diff --git a/src/Umbraco.Tests/Routing/ContentFinderByUrlWithDomainsTests.cs b/src/Umbraco.Tests/Routing/ContentFinderByUrlWithDomainsTests.cs index ac2e62b1efdb..18eafe508ea8 100644 --- a/src/Umbraco.Tests/Routing/ContentFinderByUrlWithDomainsTests.cs +++ b/src/Umbraco.Tests/Routing/ContentFinderByUrlWithDomainsTests.cs @@ -134,7 +134,7 @@ public void Lookup_SingleDomain(string url, int expectedId) var publishedRouter = CreatePublishedRouter(Factory); var frequest = publishedRouter.CreateRequest(umbracoContext); - // must lookup domain else lookup by url fails + // must lookup domain else lookup by URL fails publishedRouter.FindDomain(frequest); var lookup = new ContentFinderByUrl(Logger); @@ -176,7 +176,7 @@ public void Lookup_NestedDomains(string url, int expectedId, string expectedCult var publishedRouter = CreatePublishedRouter(Factory); var frequest = publishedRouter.CreateRequest(umbracoContext); - // must lookup domain else lookup by url fails + // must lookup domain else lookup by URL fails publishedRouter.FindDomain(frequest); Assert.AreEqual(expectedCulture, frequest.Culture.Name); diff --git a/src/Umbraco.Tests/Routing/RouteTestExtensions.cs b/src/Umbraco.Tests/Routing/RouteTestExtensions.cs index fae8e2a6fa36..642488c25627 100644 --- a/src/Umbraco.Tests/Routing/RouteTestExtensions.cs +++ b/src/Umbraco.Tests/Routing/RouteTestExtensions.cs @@ -9,7 +9,7 @@ public static class RouteTestExtensions { /// - /// Return the route data for the url based on a mocked context + /// Return the route data for the URL based on a mocked context /// /// /// @@ -21,7 +21,7 @@ public static RouteData GetDataForRoute(this RouteCollection routes, string requ } /// - /// Get the route data based on the url and http context + /// Get the route data based on the URL and HTTP context /// /// /// diff --git a/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs b/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs index e95fdfc87c59..94660d40cf5c 100644 --- a/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs +++ b/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs @@ -100,7 +100,7 @@ public void Is_Client_Side_Request(string url, bool assert) [Test] public void Is_Client_Side_Request_InvalidPath_ReturnFalse() { - //This url is invalid. Default to false when the extension cannot be determined + //This URL is invalid. Default to false when the extension cannot be determined var uri = new Uri("http://test.com/installing-modules+foobar+\"yipee\""); var result = uri.IsClientSideRequest(); Assert.AreEqual(false, result); diff --git a/src/Umbraco.Tests/Routing/UrlProviderTests.cs b/src/Umbraco.Tests/Routing/UrlProviderTests.cs index 8043e256619e..d61517b41e93 100644 --- a/src/Umbraco.Tests/Routing/UrlProviderTests.cs +++ b/src/Umbraco.Tests/Routing/UrlProviderTests.cs @@ -184,7 +184,7 @@ public void Get_Url_For_Culture_Variant_Without_Domains_Non_Current_Url() globalSettings: globalSettings.Object, snapshotService: snapshotService.Object); - //even though we are asking for a specific culture URL, there are no domains assigned so all that can be returned is a normal relative url. + //even though we are asking for a specific culture URL, there are no domains assigned so all that can be returned is a normal relative URL. var url = umbracoContext.UrlProvider.GetUrl(1234, culture: "fr-FR"); Assert.AreEqual("/home/test-fr/", url); diff --git a/src/Umbraco.Tests/Routing/UrlRoutesTests.cs b/src/Umbraco.Tests/Routing/UrlRoutesTests.cs index 4b8d708df685..724a263b542b 100644 --- a/src/Umbraco.Tests/Routing/UrlRoutesTests.cs +++ b/src/Umbraco.Tests/Routing/UrlRoutesTests.cs @@ -12,7 +12,7 @@ namespace Umbraco.Tests.Routing { // purpose: test the values returned by PublishedContentCache.GetRouteById - // and .GetByRoute (no caching at all, just routing nice urls) including all + // and .GetByRoute (no caching at all, just routing nice URLs) including all // the quirks due to hideTopLevelFromPath and backward compatibility. [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] @@ -146,7 +146,7 @@ cache route (as not trusted) node = node(id) - walk up from node to domain or root, assemble parts = url segments + walk up from node to domain or root, assemble parts = URL segments if !domain and global.hide: if id.parent: diff --git a/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs b/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs index 6587b2e4f6cf..363f87395d96 100644 --- a/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs +++ b/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs @@ -16,7 +16,7 @@ namespace Umbraco.Tests.Routing [TestFixture] public class UrlsWithNestedDomains : UrlRoutingTestBase { - // in the case of nested domains more than 1 url may resolve to a document + // in the case of nested domains more than 1 URL may resolve to a document // but only one route can be cached - the 'canonical' route ie the route // using the closest domain to the node - here we test that if we request // a non-canonical route, it is not cached / the cache is not polluted @@ -40,7 +40,7 @@ public void DoNotPolluteCache() const string url = "http://domain1.com/1001-1/1001-1-1"; - // get the nice url for 100111 + // get the nice URL for 100111 var umbracoContext = GetUmbracoContext(url, 9999, umbracoSettings: settings, urlProviders: new [] { new DefaultUrlProvider(settings.RequestHandler, Logger, globalSettings.Object, new SiteDomainHelper()) @@ -53,7 +53,7 @@ public void DoNotPolluteCache() var cachedRoutes = cache.RoutesCache.GetCachedRoutes(); Assert.AreEqual("10011/1001-1-1", cachedRoutes[100111]); - // route a rogue url + // route a rogue URL var publishedRouter = CreatePublishedRouter(); var frequest = publishedRouter.CreateRequest(umbracoContext); @@ -71,7 +71,7 @@ public void DoNotPolluteCache() Assert.AreEqual("10011/1001-1-1", cachedRoutes[100111]); // no //Assert.AreEqual("1001/1001-1/1001-1-1", cachedRoutes[100111]); // yes - // what's the nice url now? + // what's the nice URL now? Assert.AreEqual("http://domain2.com/1001-1-1/", umbracoContext.UrlProvider.GetUrl(100111)); // good //Assert.AreEqual("http://domain1.com/1001-1/1001-1-1", routingContext.NiceUrlProvider.GetNiceUrl(100111, true)); // bad } diff --git a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs index 9157a76773b0..93b4dd0766ca 100644 --- a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs +++ b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs @@ -201,7 +201,7 @@ public void StandaloneTest() Assert.AreEqual("test", pcontent.Name()); Assert.IsTrue(pcontent.IsDraft()); - // no published url + // no published URL Assert.AreEqual("#", pcontent.Url()); // now publish the document + make some unpublished changes @@ -215,7 +215,7 @@ public void StandaloneTest() Assert.AreEqual("test", pcontent.Name()); Assert.IsFalse(pcontent.IsDraft()); - // but the url is the published one - no draft url + // but the URL is the published one - no draft URL Assert.AreEqual("/test/", pcontent.Url()); // and also an updated draft document @@ -224,7 +224,7 @@ public void StandaloneTest() Assert.AreEqual("testx", pcontent.Name()); Assert.IsTrue(pcontent.IsDraft()); - // and the published document has a url + // and the published document has a URL Assert.AreEqual("/test/", pcontent.Url()); umbracoContextReference.Dispose(); diff --git a/src/Umbraco.Tests/Services/EntityServiceTests.cs b/src/Umbraco.Tests/Services/EntityServiceTests.cs index 0598b8cea285..75f3662ee2af 100644 --- a/src/Umbraco.Tests/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests/Services/EntityServiceTests.cs @@ -8,12 +8,12 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Services; -using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; namespace Umbraco.Tests.Services { + /// /// Tests covering the EntityService /// diff --git a/src/Umbraco.Tests/Services/ExternalLoginServiceTests.cs b/src/Umbraco.Tests/Services/ExternalLoginServiceTests.cs new file mode 100644 index 000000000000..8a31518ca08d --- /dev/null +++ b/src/Umbraco.Tests/Services/ExternalLoginServiceTests.cs @@ -0,0 +1,249 @@ +using System; +using System.Linq; +using System.Threading; +using Microsoft.AspNet.Identity; +using NUnit.Framework; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Testing; + +namespace Umbraco.Tests.Services +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] + public class ExternalLoginServiceTests : TestWithDatabaseBase + { + [Test] + public void Removes_Existing_Duplicates_On_Save() + { + var user = new User("Test", "test@test.com", "test", "helloworldtest"); + ServiceContext.UserService.Save(user); + + var providerKey = Guid.NewGuid().ToString("N"); + var latest = DateTime.Now.AddDays(-1); + var oldest = DateTime.Now.AddDays(-10); + + using (var scope = ScopeProvider.CreateScope()) + { + // insert duplicates manuall + scope.Database.Insert(new ExternalLoginDto + { + UserId = user.Id, + LoginProvider = "test1", + ProviderKey = providerKey, + CreateDate = latest + }); + scope.Database.Insert(new ExternalLoginDto + { + UserId = user.Id, + LoginProvider = "test1", + ProviderKey = providerKey, + CreateDate = oldest + }); + } + + // try to save 2 other duplicates + var externalLogins = new[] + { + new ExternalLogin("test2", providerKey), + new ExternalLogin("test2", providerKey), + new ExternalLogin("test1", providerKey) + }; + + ServiceContext.ExternalLoginService.Save(user.Id, externalLogins); + + var logins = ServiceContext.ExternalLoginService.GetAll(user.Id).ToList(); + + // duplicates will be removed, keeping the latest entries + Assert.AreEqual(2, logins.Count); + + var test1 = logins.Single(x => x.LoginProvider == "test1"); + Assert.Greater(test1.CreateDate, latest); + } + + [Test] + public void Does_Not_Persist_Duplicates() + { + var user = new User("Test", "test@test.com", "test", "helloworldtest"); + ServiceContext.UserService.Save(user); + + var providerKey = Guid.NewGuid().ToString("N"); + var externalLogins = new[] + { + new ExternalLogin("test1", providerKey), + new ExternalLogin("test1", providerKey) + }; + + ServiceContext.ExternalLoginService.Save(user.Id, externalLogins); + + var logins = ServiceContext.ExternalLoginService.GetAll(user.Id).ToList(); + Assert.AreEqual(1, logins.Count); + } + + [Test] + public void Single_Create() + { + var user = new User("Test", "test@test.com", "test", "helloworldtest"); + ServiceContext.UserService.Save(user); + + var extLogin = new IdentityUserLogin("test1", Guid.NewGuid().ToString("N"), user.Id) + { + UserData = "hello" + }; + ServiceContext.ExternalLoginService.Save(extLogin); + + var found = ServiceContext.ExternalLoginService.GetAll(user.Id); + + Assert.AreEqual(1, found.Count()); + Assert.IsTrue(extLogin.HasIdentity); + Assert.IsTrue(extLogin.Id > 0); + } + + [Test] + public void Single_Update() + { + var user = new User("Test", "test@test.com", "test", "helloworldtest"); + ServiceContext.UserService.Save(user); + + var extLogin = new IdentityUserLogin("test1", Guid.NewGuid().ToString("N"), user.Id) + { + UserData = "hello" + }; + ServiceContext.ExternalLoginService.Save(extLogin); + + extLogin.UserData = "world"; + ServiceContext.ExternalLoginService.Save(extLogin); + + var found = ServiceContext.ExternalLoginService.GetAll(user.Id).Cast().ToList(); + Assert.AreEqual(1, found.Count); + Assert.AreEqual("world", found[0].UserData); + } + + [Test] + public void Multiple_Update() + { + var user = new User("Test", "test@test.com", "test", "helloworldtest"); + ServiceContext.UserService.Save(user); + + var providerKey1 = Guid.NewGuid().ToString("N"); + var providerKey2 = Guid.NewGuid().ToString("N"); + var extLogins = new[] + { + new ExternalLogin("test1", providerKey1, "hello"), + new ExternalLogin("test2", providerKey2, "world") + }; + ServiceContext.ExternalLoginService.Save(user.Id, extLogins); + + extLogins = new[] + { + new ExternalLogin("test1", providerKey1, "123456"), + new ExternalLogin("test2", providerKey2, "987654") + }; + ServiceContext.ExternalLoginService.Save(user.Id, extLogins); + + var found = ServiceContext.ExternalLoginService.GetAll(user.Id).Cast().OrderBy(x => x.LoginProvider).ToList(); + Assert.AreEqual(2, found.Count); + Assert.AreEqual("123456", found[0].UserData); + Assert.AreEqual("987654", found[1].UserData); + } + + [Test] + public void Can_Find_As_Extended_Type() + { + var user = new User("Test", "test@test.com", "test", "helloworldtest"); + ServiceContext.UserService.Save(user); + + var providerKey1 = Guid.NewGuid().ToString("N"); + var providerKey2 = Guid.NewGuid().ToString("N"); + var extLogins = new[] + { + new ExternalLogin("test1", providerKey1, "hello"), + new ExternalLogin("test2", providerKey2, "world") + }; + ServiceContext.ExternalLoginService.Save(user.Id, extLogins); + + var found = ServiceContext.ExternalLoginService.Find("test2", providerKey2).ToList(); + Assert.AreEqual(1, found.Count); + var asExtended = found.Cast().ToList(); + Assert.AreEqual(1, found.Count); + + } + + [Test] + public void Add_Logins() + { + var user = new User("Test", "test@test.com", "test", "helloworldtest"); + ServiceContext.UserService.Save(user); + + var externalLogins = new[] + { + new ExternalLogin("test1", Guid.NewGuid().ToString("N")), + new ExternalLogin("test2", Guid.NewGuid().ToString("N")) + }; + + ServiceContext.ExternalLoginService.Save(user.Id, externalLogins); + + var logins = ServiceContext.ExternalLoginService.GetAll(user.Id).OrderBy(x => x.LoginProvider).ToList(); + Assert.AreEqual(2, logins.Count); + for (int i = 0; i < logins.Count; i++) + { + Assert.AreEqual(logins[i].ProviderKey, externalLogins[i].ProviderKey); + Assert.AreEqual(logins[i].LoginProvider, externalLogins[i].LoginProvider); + } + } + + [Test] + public void Add_Update_Delete_Logins() + { + var user = new User("Test", "test@test.com", "test", "helloworldtest"); + ServiceContext.UserService.Save(user); + + var externalLogins = new[] + { + new ExternalLogin("test1", Guid.NewGuid().ToString("N")), + new ExternalLogin("test2", Guid.NewGuid().ToString("N")), + new ExternalLogin("test3", Guid.NewGuid().ToString("N")), + new ExternalLogin("test4", Guid.NewGuid().ToString("N")) + }; + + ServiceContext.ExternalLoginService.Save(user.Id, externalLogins); + + var logins = ServiceContext.ExternalLoginService.GetAll(user.Id).OrderBy(x => x.LoginProvider).ToList(); + + logins.RemoveAt(0); // remove the first one + logins.Add(new IdentityUserLogin("test5", Guid.NewGuid().ToString("N"), user.Id)); // add a new one + + // save new list + ServiceContext.ExternalLoginService.Save(user.Id, logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey))); + + var updatedLogins = ServiceContext.ExternalLoginService.GetAll(user.Id).OrderBy(x => x.LoginProvider).ToList(); + Assert.AreEqual(4, updatedLogins.Count); + for (int i = 0; i < updatedLogins.Count; i++) + { + Assert.AreEqual(logins[i].LoginProvider, updatedLogins[i].LoginProvider); + } + } + + [Test] + public void Add_Retrieve_User_Data() + { + var user = new User("Test", "test@test.com", "test", "helloworldtest"); + ServiceContext.UserService.Save(user); + + var externalLogins = new[] + { + new ExternalLogin("test1", Guid.NewGuid().ToString("N"), "hello world") + }; + + ServiceContext.ExternalLoginService.Save(user.Id, externalLogins); + + var logins = ServiceContext.ExternalLoginService.GetAll(user.Id).Cast().ToList(); + + Assert.AreEqual("hello world", logins[0].UserData); + + } + } +} diff --git a/src/Umbraco.Tests/Strings/DefaultShortStringHelperTests.cs b/src/Umbraco.Tests/Strings/DefaultShortStringHelperTests.cs index 5fd5710a7989..b93d79dcf048 100644 --- a/src/Umbraco.Tests/Strings/DefaultShortStringHelperTests.cs +++ b/src/Umbraco.Tests/Strings/DefaultShortStringHelperTests.cs @@ -338,7 +338,7 @@ public void CleanStringSymbols() Assert.AreEqual("house*2", helper.CleanString("house (2)", CleanStringType.Alias)); // FIXME: but for a filename we want to keep them! - // FIXME: and what about a url? + // FIXME: and what about a URL? } [Test] @@ -424,7 +424,7 @@ public void CleanStringDefaultConfig() // lower-cased, utf8 filename, removing illegal filename chars, using dash-separator Assert.AreEqual("0123-中文测试-中文测试-léger-zôrg-2-a-x", filename, "filename"); - // lower-cased, utf8 url segment, only letters and digits, using dash-separator + // lower-cased, utf8 URL segment, only letters and digits, using dash-separator Assert.AreEqual("0123-中文测试-中文测试-léger-zôrg-2-a-x", segment, "segment"); } diff --git a/src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs b/src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs index 6c40e2842d22..59968024d1f6 100644 --- a/src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs +++ b/src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs @@ -63,7 +63,7 @@ public void Remove_Image_Sources() [Test] public void Ensure_Image_Sources() { - //setup a mock url provider which we'll use for testing + //setup a mock URL provider which we'll use for testing var mediaType = new PublishedContentType(Guid.NewGuid(), 777, "image", PublishedItemType.Media, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var media = new Mock(); diff --git a/src/Umbraco.Tests/Templates/HtmlLocalLinkParserTests.cs b/src/Umbraco.Tests/Templates/HtmlLocalLinkParserTests.cs index 861a7e4db674..f9f82fd31bcf 100644 --- a/src/Umbraco.Tests/Templates/HtmlLocalLinkParserTests.cs +++ b/src/Umbraco.Tests/Templates/HtmlLocalLinkParserTests.cs @@ -49,7 +49,7 @@ public void Returns_Udis_From_LocalLinks() [TestCase("hello href=\"{localLink:umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"#\" world ")] public void ParseLocalLinks(string input, string result) { - //setup a mock url provider which we'll use for testing + //setup a mock URL provider which we'll use for testing var contentUrlProvider = new Mock(); contentUrlProvider .Setup(x => x.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index 0840d8a18ca4..cc1cfa6a1dd5 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -81,7 +81,7 @@ public void RegisterServices(IRegister register) /// /// /// An event messages factory. - /// Some url segment providers. + /// Some URL segment providers. /// /// A container. /// diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 004945bb4643..6296a8203f51 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -167,6 +167,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/cy.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/cy.js index d2fdd077f527..2863840abb1d 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/cy.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/cy.js @@ -121,7 +121,7 @@ tinymce.addI18n('cy',{ "Date\/time": "Dyddiad\/amser", "Insert date\/time": "Mewnosod dyddiad\/amser", "Remove link": "Tynnu dolen", -"Url": "Url", +"Url": "URL", "Text to display": "Testun i'w ddangos", "Anchors": "Angorau", "Insert link": "Mewnosod dolen", @@ -227,4 +227,4 @@ tinymce.addI18n('cy',{ "View": "Dangos", "Table": "Tabl", "Format": "Fformat" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/da.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/da.js index 90e4009d92ba..a50f2f10ec44 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/da.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/da.js @@ -150,7 +150,7 @@ tinymce.addI18n('da',{ "Insert link": "Inds\u00e6t link", "Insert\/edit link": "Inds\u00e6t\/ret link", "Text to display": "Vis tekst", -"Url": "Url", +"Url": "URL", "Target": "Target", "None": "Ingen", "New window": "Nyt vindue", @@ -258,4 +258,4 @@ tinymce.addI18n('da',{ "Tools": "V\u00e6rkt\u00f8j", "Powered by {0}": "Drevet af {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Rich Text omr\u00e5de. Tryk ALT-F9 for menu. Tryk ALT-F10 for toolbar. Tryk ALT-0 for hj\u00e6lp" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/de_AT.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/de_AT.js index 2af071f5ce5b..0073810c1a3d 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/de_AT.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/de_AT.js @@ -150,7 +150,7 @@ tinymce.addI18n('de_AT',{ "Insert link": "Link einf\u00fcgen", "Insert\/edit link": "Link einf\u00fcgen\/bearbeiten", "Text to display": "Angezeigter Text", -"Url": "Url", +"Url": "URL", "Target": "Ziel", "None": "Keine", "New window": "Neues Fenster", @@ -258,4 +258,4 @@ tinymce.addI18n('de_AT',{ "Tools": "Extras", "Powered by {0}": "Betrieben von {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Rich Text Area. Dr\u00fccken Sie ALT-F9 f\u00fcr das Men\u00fc. Dr\u00fccken Sie ALT-F10 f\u00fcr die Werkzeugleiste. Dr\u00fccken Sie ALT-0 f\u00fcr Hilfe" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_CA.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_CA.js index f32de017246e..cc07ffd23efa 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_CA.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_CA.js @@ -150,7 +150,7 @@ tinymce.addI18n('en_CA',{ "Insert link": "Insert link", "Insert\/edit link": "Insert\/edit link", "Text to display": "Text to display", -"Url": "Url", +"Url": "URL", "Target": "Target", "None": "None", "New window": "New window", @@ -258,4 +258,4 @@ tinymce.addI18n('en_CA',{ "Tools": "Tools", "Powered by {0}": "Powered by {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_US.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_US.js index 90eae85800c8..0b50212fd92e 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_US.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_US.js @@ -150,7 +150,7 @@ tinymce.addI18n('en_US',{ "Insert link": "Insert link", "Insert\/edit link": "Insert\/edit link", "Text to display": "Text to display", -"Url": "Url", +"Url": "URL", "Target": "Target", "None": "None", "New window": "New window", @@ -258,4 +258,4 @@ tinymce.addI18n('en_US',{ "Tools": "Tools", "Powered by {0}": "Powered by {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/es_MX.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/es_MX.js index b0f2019ffe30..688f14ba5119 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/es_MX.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/es_MX.js @@ -150,7 +150,7 @@ tinymce.addI18n('es_MX',{ "Insert link": "Insertar enlace", "Insert\/edit link": "Inserta\/editar enlace", "Text to display": "Texto a mostrar", -"Url": "Url", +"Url": "URL", "Target": "Objetivo", "None": "Ninguno", "New window": "Nueva ventana", @@ -258,4 +258,4 @@ tinymce.addI18n('es_MX',{ "Tools": "Herramientas", "Powered by {0}": "Creado con {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Presione dentro del \u00e1rea de texto ALT-F9 para invocar el men\u00fa, ALT-F10 para la barra de herramientas y ALT-0 para la ayuda." -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/et.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/et.js index c6beeeb51072..96b763506c41 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/et.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/et.js @@ -150,7 +150,7 @@ tinymce.addI18n('et',{ "Insert link": "Lisa link", "Insert\/edit link": "Lisa\/muuda link", "Text to display": "Kuvatav tekst", -"Url": "Viide (url)", +"Url": "Viide (URL)", "Target": "Sihtm\u00e4rk", "None": "Puudub", "New window": "Uus aken", @@ -258,4 +258,4 @@ tinymce.addI18n('et',{ "Tools": "T\u00f6\u00f6riistad", "Powered by {0}": "Kasutatud tarkvara {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Rikastatud teksti ala. Men\u00fc\u00fc jaoks vajuta ALT-F9. T\u00f6\u00f6riistariba jaoks vajuta ALT-F10. Abi saamiseks vajuta ALT-0." -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/eu.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/eu.js index c4374394d444..44e62db3f8f3 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/eu.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/eu.js @@ -150,7 +150,7 @@ tinymce.addI18n('eu',{ "Insert link": "Esteka txertatu", "Insert\/edit link": "Esteka txertatu\/editatu", "Text to display": "Bistaratzeko testua", -"Url": "Url", +"Url": "URL", "Target": "Target", "None": "Bat ere ez", "New window": "Lehio berria", @@ -258,4 +258,4 @@ tinymce.addI18n('eu',{ "Tools": "Tresnak", "Powered by {0}": "{0}rekin egina", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Testu aberastuko area. Sakatu ALT-F9 menurako. Sakatu ALT-F10 tresna-barrarako. Sakatu ALT-0 laguntzarako" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/fr.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/fr.js index 2d074f8c6e1c..d61015efa0aa 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/fr.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/fr.js @@ -150,7 +150,7 @@ tinymce.addI18n('fr_FR',{ "Insert\/Edit Link": "Ins\u00e9rer\/Modifier lien", "Insert\/edit link": "Ins\u00e9rer\/modifier un lien", "Text to display": "Texte \u00e0 afficher", -"Url": "Url", +"Url": "URL", "Open link in...": "Ouvrir le lien dans...", "Current window": "Fen\u00eatre active", "None": "n\/a", @@ -386,4 +386,4 @@ tinymce.addI18n('fr_FR',{ "Spellcheck": "V\u00e9rification orthographique", "Caption": "Titre", "Insert template": "Ajouter un th\u00e8me" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/fr_FR.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/fr_FR.js index 5c37164b2c24..5ed177c9fc14 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/fr_FR.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/fr_FR.js @@ -150,7 +150,7 @@ tinymce.addI18n('fr_FR',{ "Insert link": "Ins\u00e9rer un lien", "Insert\/edit link": "Ins\u00e9rer\/modifier un lien", "Text to display": "Texte \u00e0 afficher", -"Url": "Url", +"Url": "URL", "Target": "Cible", "None": "n\/a", "New window": "Nouvelle fen\u00eatre", @@ -258,4 +258,4 @@ tinymce.addI18n('fr_FR',{ "Tools": "Outils", "Powered by {0}": "Propuls\u00e9 par {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Zone Texte Riche. Appuyer sur ALT-F9 pour le menu. Appuyer sur ALT-F10 pour la barre d'outils. Appuyer sur ALT-0 pour de l'aide." -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/hr.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/hr.js index d52f861ce9d6..617e1f4823b2 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/hr.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/hr.js @@ -144,7 +144,7 @@ tinymce.addI18n('hr',{ "Insert link": "Umetni poveznicu", "Insert\/edit link": "Umetni\/izmijeni poveznicu", "Text to display": "Tekst za prikaz", -"Url": "Url", +"Url": "URL", "Target": "Meta", "None": "Ni\u0161ta", "New window": "Novi prozor", @@ -250,4 +250,4 @@ tinymce.addI18n('hr',{ "Table": "Tablica", "Tools": "Alati", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Pritisni ALT-F9 za izbornik. Pritisni ALT-F10 za alatnu traku. Pritisni ALT-0 za pomo\u0107" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/hu_HU.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/hu_HU.js index 3972dc2b3c2d..13bb4984a9f0 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/hu_HU.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/hu_HU.js @@ -150,7 +150,7 @@ tinymce.addI18n('hu_HU',{ "Insert link": "Hivatkoz\u00e1s beilleszt\u00e9se", "Insert\/edit link": "Hivatkoz\u00e1s beilleszt\u00e9se\/szerkeszt\u00e9se", "Text to display": "Megjelen\u0151 sz\u00f6veg", -"Url": "Url", +"Url": "URL", "Target": "C\u00e9l", "None": "Nincs", "New window": "\u00daj ablak", @@ -258,4 +258,4 @@ tinymce.addI18n('hu_HU',{ "Tools": "Eszk\u00f6z\u00f6k", "Powered by {0}": "\u00dczemelteti: {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Rich Text ter\u00fclet. Nyomj ALT-F9-et a men\u00fch\u00f6z. Nyomj ALT-F10-et az eszk\u00f6zt\u00e1rhoz. Nyomj ALT-0-t a s\u00fag\u00f3hoz" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/it.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/it.js index 5ffc0c0f8058..a97118da2140 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/it.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/it.js @@ -150,7 +150,7 @@ tinymce.addI18n('it',{ "Insert link": "Inserisci il Link", "Insert\/edit link": "Inserisci\/Modifica Link", "Text to display": "Testo da Visualizzare", -"Url": "Url", +"Url": "URL", "Target": "Target", "None": "No", "New window": "Nuova Finestra", @@ -258,4 +258,4 @@ tinymce.addI18n('it',{ "Tools": "Strumenti", "Powered by {0}": "Fornito da {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Rich Text Area. Premi ALT-F9 per il men\u00f9. Premi ALT-F10 per la barra degli strumenti. Premi ALT-0 per l'aiuto." -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/ka_GE.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/ka_GE.js index 9bffb8040d5a..805a96648958 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/ka_GE.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/ka_GE.js @@ -121,7 +121,7 @@ tinymce.addI18n('ka_GE',{ "Date\/time": "\u10d7\u10d0\u10e0\u10d8\u10e6\u10d8\/\u10d3\u10e0\u10dd", "Insert date\/time": "\u10d7\u10d0\u10e0\u10d8\u10e6\u10d8\/\u10d3\u10e0\u10dd\u10d8\u10e1 \u10e9\u10d0\u10e1\u10db\u10d0", "Remove link": "\u10d1\u10db\u10e3\u10da\u10d8\u10e1 \u10ec\u10d0\u10e8\u10da\u10d0", -"Url": "Url", +"Url": "URL", "Text to display": "\u10e2\u10d4\u10e5\u10e1\u10e2\u10d8", "Anchors": "\u10e6\u10e3\u10d6\u10d0", "Insert link": "\u10d1\u10db\u10e3\u10da\u10d8\u10e1 \u10e9\u10d0\u10e1\u10db\u10d0", @@ -227,4 +227,4 @@ tinymce.addI18n('ka_GE',{ "View": "\u10dc\u10d0\u10ee\u10d5\u10d0", "Table": "\u10ea\u10ee\u10e0\u10d8\u10da\u10d8", "Format": "\u10e4\u10dd\u10e0\u10db\u10d0\u10e2\u10d8" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/kab.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/kab.js index b9f9bccf40e9..48f7d3bf5580 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/kab.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/kab.js @@ -150,7 +150,7 @@ tinymce.addI18n('kab',{ "Insert link": "Ger azday", "Insert\/edit link": "Ger\/\u1e93reg azday", "Text to display": "A\u1e0dris ara yettwabeqq\u1e0den", -"Url": "Url", +"Url": "URL", "Target": "Target", "None": "Ulac", "New window": "Asfaylu amaynut", @@ -258,4 +258,4 @@ tinymce.addI18n('kab',{ "Tools": "Ifecka", "Powered by {0}": "Iteddu s {0} ", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/km_KH.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/km_KH.js index 5c4b05512664..381d4c4a4615 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/km_KH.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/km_KH.js @@ -144,7 +144,7 @@ tinymce.addI18n('km_KH',{ "Insert link": "\u1794\u1789\u17d2\u1785\u17bc\u179b\u200b\u178f\u17c6\u178e", "Insert\/edit link": "\u1794\u1789\u17d2\u1785\u17bc\u179b\/\u1780\u17c2 \u178f\u17c6\u178e", "Text to display": "\u17a2\u1780\u17d2\u179f\u179a\u200b\u178f\u17d2\u179a\u17bc\u179c\u200b\u1794\u1784\u17d2\u17a0\u17b6\u1789", -"Url": "Url", +"Url": "URL", "Target": "\u1791\u17b7\u179f\u178a\u17c5", "None": "\u1798\u17b7\u1793\u200b\u1798\u17b6\u1793", "New window": "\u1795\u17d2\u1791\u17b6\u17c6\u1784\u200b\u179c\u17b8\u1793\u178a\u17bc\u200b\u1790\u17d2\u1798\u17b8", @@ -250,4 +250,4 @@ tinymce.addI18n('km_KH',{ "Table": "\u178f\u17b6\u179a\u17b6\u1784", "Tools": "\u17a7\u1794\u1780\u179a\u178e\u17cd", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u1791\u17b8\u178f\u17b6\u17c6\u1784\u200b\u17a2\u1780\u17d2\u179f\u179a\u200b\u179f\u17c6\u1794\u17bc\u179a\u1794\u17c2\u1794\u17d4 \u1785\u17bb\u1785 ALT-F9 \u179f\u1798\u17d2\u179a\u17b6\u1794\u17cb\u200b\u1798\u17c9\u17ba\u1793\u17bb\u1799\u17d4 \u1785\u17bb\u1785 ALT-F10 \u179f\u1798\u17d2\u179a\u17b6\u1794\u17cb\u200b\u179a\u1794\u17b6\u179a\u200b\u17a7\u1794\u1780\u179a\u178e\u17cd\u17d4 \u1785\u17bb\u1785 ALT-0 \u179f\u1798\u17d2\u179a\u17b6\u1794\u17cb\u200b\u1787\u17c6\u1793\u17bd\u1799\u17d4" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/nb_NO.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/nb_NO.js index 59233450b0e6..e6f1df897126 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/nb_NO.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/nb_NO.js @@ -150,7 +150,7 @@ tinymce.addI18n('nb_NO',{ "Insert link": "Sett inn lenke", "Insert\/edit link": "Sett inn\/endre lenke", "Text to display": "Tekst som skal vises", -"Url": "Url", +"Url": "URL", "Target": "M\u00e5l", "None": "Ingen", "New window": "Nytt vindu", @@ -258,4 +258,4 @@ tinymce.addI18n('nb_NO',{ "Tools": "Verkt\u00f8y", "Powered by {0}": "Redigert med {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Tekstredigering. Tast ALT-F9 for meny. Tast ALT-F10 for verkt\u00f8ys-rader. Tast ALT-0 for hjelp." -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/nl.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/nl.js index c80590b2978c..d8631c9ab6cb 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/nl.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/nl.js @@ -150,7 +150,7 @@ tinymce.addI18n('nl',{ "Insert link": "Hyperlink invoegen", "Insert\/edit link": "Hyperlink invoegen\/bewerken", "Text to display": "Linktekst", -"Url": "Url", +"Url": "URL", "Target": "Doel", "None": "Geen", "New window": "Nieuw venster", @@ -258,4 +258,4 @@ tinymce.addI18n('nl',{ "Tools": "Gereedschap", "Powered by {0}": "Gemaakt door {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Rich Text Area. Druk ALT-F9 voor het menu. Druk ALT-F10 voor de toolbar. Druk ALT-0 voor help." -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/pt_BR.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/pt_BR.js index 497043a9d4de..2beccd413b87 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/pt_BR.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/pt_BR.js @@ -150,7 +150,7 @@ tinymce.addI18n('pt_BR',{ "Insert link": "Inserir link", "Insert\/edit link": "Inserir\/editar link", "Text to display": "Texto para mostrar", -"Url": "Url", +"Url": "URL", "Target": "Alvo", "None": "Nenhum", "New window": "Nova janela", @@ -258,4 +258,4 @@ tinymce.addI18n('pt_BR',{ "Tools": "Ferramentas", "Powered by {0}": "Distribu\u00eddo por {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u00c1rea de texto formatado. Pressione ALT-F9 para exibir o menu, ALT-F10 para exibir a barra de ferramentas ou ALT-0 para exibir a ajuda" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/ro.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/ro.js index 0a6a2eadf014..a2f3caec805b 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/ro.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/ro.js @@ -121,7 +121,7 @@ tinymce.addI18n('ro',{ "Date\/time": "Data\/ora", "Insert date\/time": "Insereaz\u0103 data\/ora", "Remove link": "\u0218terge link-ul", -"Url": "Url", +"Url": "URL", "Text to display": "Text de afi\u0219at", "Anchors": "Ancor\u0103", "Insert link": "Inserare link", @@ -227,4 +227,4 @@ tinymce.addI18n('ro',{ "View": "Vezi", "Table": "Tabel\u0103", "Format": "Formateaz\u0103" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/sk.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/sk.js index 2362a6dbc6ce..5cc085abdc5f 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/sk.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/sk.js @@ -144,7 +144,7 @@ tinymce.addI18n('sk',{ "Insert link": "Vlo\u017ei\u0165 odkaz", "Insert\/edit link": "Vlo\u017ei\u0165\/upravi\u0165 odkaz", "Text to display": "Zobrazen\u00fd text", -"Url": "Url", +"Url": "URL", "Target": "Cie\u013e", "None": "\u017diadne", "New window": "Nov\u00e9 okno", @@ -250,4 +250,4 @@ tinymce.addI18n('sk',{ "Table": "Tabu\u013eka", "Tools": "N\u00e1stroje", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Textov\u00e9 pole. Stla\u010dte ALT-F9 pre zobrazenie menu, ALT-F10 pre zobrazenie panela n\u00e1strojov, ALT-0 pre n\u00e1povedu." -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/sr.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/sr.js index 9150c2ed8211..01bfad730307 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/sr.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/sr.js @@ -150,7 +150,7 @@ tinymce.addI18n('sr',{ "Insert link": "Ubaci vezu", "Insert\/edit link": "Ubaci\/promeni vezu", "Text to display": "Tekst za prikaz", -"Url": "Url", +"Url": "URL", "Target": "Meta", "None": "Ni\u0161ta", "New window": "Novi prozor", @@ -258,4 +258,4 @@ tinymce.addI18n('sr',{ "Tools": "Alatke", "Powered by {0}": "Pokre\u0107e ga {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Oboga\u0107en tekst. Pritisni te ALT-F9 za meni.Pritisnite ALT-F10 za traku sa alatkama.Pritisnite ALT-0 za pomo\u0107" -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/sv_SE.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/sv_SE.js index 83aaaef59d45..b97ea68a18d0 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/sv_SE.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/sv_SE.js @@ -150,7 +150,7 @@ tinymce.addI18n('sv_SE',{ "Insert link": "Infoga l\u00e4nk", "Insert\/edit link": "Infoga\/redigera l\u00e4nk", "Text to display": "Text att visa", -"Url": "Url", +"Url": "URL", "Target": "M\u00e5l", "None": "Ingen", "New window": "Nytt f\u00f6nster", @@ -158,8 +158,8 @@ tinymce.addI18n('sv_SE',{ "Anchors": "Bokm\u00e4rken", "Link": "L\u00e4nk", "Paste or type a link": "Klistra in eller skriv en l\u00e4nk", -"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "Urlen du angav verkar vara en epost adress. Vill du l\u00e4gga till ett mailto: prefix?", -"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "Urlen du angav verkar vara en extern l\u00e4nk. Vill du l\u00e4gga till http:\/\/ prefixet?", +"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "URLen du angav verkar vara en epost adress. Vill du l\u00e4gga till ett mailto: prefix?", +"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "URLen du angav verkar vara en extern l\u00e4nk. Vill du l\u00e4gga till http:\/\/ prefixet?", "Link list": "L\u00e4nklista", "Insert video": "Infoga video", "Insert\/edit video": "Infoga\/redigera video", @@ -258,4 +258,4 @@ tinymce.addI18n('sv_SE',{ "Tools": "Verktyg", "Powered by {0}": "Powered by {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Textredigerare. Tryck ALT-F9 f\u00f6r menyn. Tryck ALT-F10 f\u00f6r verktygsrader. Tryck ALT-0 f\u00f6r hj\u00e4lp." -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/tr.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/tr.js index 7b6959640230..3dd22ca25f7c 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/tr.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/tr.js @@ -150,7 +150,7 @@ tinymce.addI18n('tr',{ "Insert link": "Ba\u011flant\u0131 ekle", "Insert\/edit link": "Ba\u011flant\u0131 ekle\/d\u00fczenle", "Text to display": "Yaz\u0131y\u0131 g\u00f6r\u00fcnt\u00fcle", -"Url": "Url", +"Url": "URL", "Target": "Hedef", "None": "Hi\u00e7biri", "New window": "Yeni pencere", @@ -258,4 +258,4 @@ tinymce.addI18n('tr',{ "Tools": "Ara\u00e7lar", "Powered by {0}": "Powered by {0}", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Zengin Metin Alan\u0131. Men\u00fc i\u00e7in ALT-F9 tu\u015funa bas\u0131n\u0131z. Ara\u00e7 \u00e7ubu\u011fu i\u00e7in ALT-F10 tu\u015funa bas\u0131n\u0131z. Yard\u0131m i\u00e7in ALT-0 tu\u015funa bas\u0131n\u0131z." -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/tr_TR.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/tr_TR.js index ea89bc44c325..496fe3dc21d7 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/tr_TR.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/tr_TR.js @@ -150,7 +150,7 @@ tinymce.addI18n('tr_TR',{ "Insert link": "Ba\u011flant\u0131 ekle", "Insert\/edit link": "Ba\u011flant\u0131 ekle\/d\u00fczenle", "Text to display": "G\u00f6r\u00fcnen yaz\u0131", -"Url": "Url", +"Url": "URL", "Target": "Hedef", "None": "Hi\u00e7biri", "New window": "Yeni pencere", @@ -258,4 +258,4 @@ tinymce.addI18n('tr_TR',{ "Tools": "Ara\u00e7lar", "Powered by {0}": "{0} taraf\u0131ndan yap\u0131lm\u0131\u015ft\u0131r ", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Zengin Metin Alan\u0131. Men\u00fc i\u00e7in ALT-F9 k\u0131sayolunu kullan\u0131n. Ara\u00e7 \u00e7ubu\u011fu i\u00e7in ALT-F10 k\u0131sayolunu kullan\u0131n. Yard\u0131m i\u00e7in ALT-0 k\u0131sayolunu kullan\u0131n." -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Black.woff b/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Black.woff deleted file mode 100644 index d1e2579bf85a..000000000000 Binary files a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Black.woff and /dev/null differ diff --git a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-BlackItalic.woff b/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-BlackItalic.woff deleted file mode 100644 index 142c1c9c48d4..000000000000 Binary files a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-BlackItalic.woff and /dev/null differ diff --git a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Bold.woff b/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Bold.woff deleted file mode 100644 index cdfcbe0fbcc5..000000000000 Binary files a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Bold.woff and /dev/null differ diff --git a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-BoldItalic.woff b/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-BoldItalic.woff deleted file mode 100644 index 3e683fea7c9b..000000000000 Binary files a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-BoldItalic.woff and /dev/null differ diff --git a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Italic.woff b/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Italic.woff deleted file mode 100644 index d8cf84c8b96e..000000000000 Binary files a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Italic.woff and /dev/null differ diff --git a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Light.woff b/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Light.woff deleted file mode 100644 index e7d4278cce83..000000000000 Binary files a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Light.woff and /dev/null differ diff --git a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-LightItalic.woff b/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-LightItalic.woff deleted file mode 100644 index bb72fd2200f1..000000000000 Binary files a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-LightItalic.woff and /dev/null differ diff --git a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Regular.woff b/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Regular.woff deleted file mode 100644 index bf73a6d9f97e..000000000000 Binary files a/src/Umbraco.Web.UI.Client/src/assets/fonts/lato/LatoLatin-Regular.woff and /dev/null differ diff --git a/src/Umbraco.Web.UI.Client/src/assets/icons/icon-adressbook.svg b/src/Umbraco.Web.UI.Client/src/assets/icons/icon-addressbook.svg similarity index 100% rename from src/Umbraco.Web.UI.Client/src/assets/icons/icon-adressbook.svg rename to src/Umbraco.Web.UI.Client/src/assets/icons/icon-addressbook.svg diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js index 43598593f441..ed20203d46e9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js @@ -13,7 +13,10 @@ } }); - function UmbLoginController($scope, $location, currentUserResource, formHelper, mediaHelper, umbRequestHelper, Upload, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, $q, $route) { + function UmbLoginController($scope, $location, currentUserResource, formHelper, + mediaHelper, umbRequestHelper, Upload, localizationService, + userService, externalLoginInfo, externalLoginInfoService, + resetPasswordCodeInfo, $timeout, authResource, $q, $route) { const vm = this; @@ -43,7 +46,15 @@ vm.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.canSendRequiredEmail && Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; vm.errorMsg = ""; vm.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; - vm.externalLoginProviders = externalLoginInfo.providers; + vm.externalLoginProviders = externalLoginInfoService.getLoginProviders(); + vm.externalLoginProviders.forEach(x => { + x.customView = externalLoginInfoService.getLoginProviderView(x); + // if there are errors set for this specific provider than assign them directly to the model + if (externalLoginInfo.errorProvider === x.authType) { + x.errors = externalLoginInfo.errors; + } + }); + vm.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin(); vm.externalLoginInfo = externalLoginInfo; vm.resetPasswordCodeInfo = resetPasswordCodeInfo; vm.backgroundImage = Umbraco.Sys.ServerVariables.umbracoSettings.loginBackgroundImage; @@ -62,7 +73,7 @@ vm.setPasswordSubmit = setPasswordSubmit; vm.labels = {}; localizationService.localizeMany([ - vm.usernameIsEmail ? "general_email" : "general_username", + vm.usernameIsEmail ? "general_email" : "general_username", vm.usernameIsEmail ? "placeholders_email" : "placeholders_usernameHint", vm.usernameIsEmail ? "placeholders_emptyEmail" : "placeholders_emptyUsername", "placeholders_emptyPassword"] @@ -72,9 +83,11 @@ vm.labels.usernameError = data[2]; vm.labels.passwordError = data[3]; }); - + vm.twoFactor = {}; + vm.loginSuccess = loginSuccess; + function onInit() { // Check if it is a new user @@ -98,11 +111,11 @@ //localize the text localizationService.localize("errorHandling_errorInPasswordFormat", [ - vm.invitedUserPasswordModel.passwordPolicies.minPasswordLength, - vm.invitedUserPasswordModel.passwordPolicies.minNonAlphaNumericChars - ]).then(function (data) { - vm.invitedUserPasswordModel.passwordPolicyText = data; - }); + vm.invitedUserPasswordModel.passwordPolicies.minPasswordLength, + vm.invitedUserPasswordModel.passwordPolicies.minNonAlphaNumericChars + ]).then(function (data) { + vm.invitedUserPasswordModel.passwordPolicyText = data; + }); }) ]).then(function () { vm.inviteStep = Number(inviteVal); @@ -144,12 +157,12 @@ function getStarted() { $location.search('invite', null); - if(vm.onLogin) { + if (vm.onLogin) { vm.onLogin(); } } - function inviteSavePassword () { + function inviteSavePassword() { if (formHelper.submitForm({ scope: $scope })) { @@ -197,37 +210,41 @@ SetTitle(); } + function loginSuccess() { + vm.loginStates.submitButton = "success"; + userService._retryRequestQueue(true); + if (vm.onLogin) { + vm.onLogin(); + } + } + function loginSubmit() { - - if (formHelper.submitForm({ scope: $scope })) { + + if (formHelper.submitForm({ scope: $scope, formCtrl: vm.loginForm })) { //if the login and password are not empty we need to automatically // validate them - this is because if there are validation errors on the server // then the user has to change both username & password to resubmit which isn't ideal, // so if they're not empty, we'll just make sure to set them to valid. - if (vm.login && vm.password && vm.login.length > 0 && vm.password.length > 0) { + if (vm.login && vm.password && vm.login.length > 0 && vm.password.length > 0) { vm.loginForm.username.$setValidity('auth', true); vm.loginForm.password.$setValidity('auth', true); } - + if (vm.loginForm.$invalid) { SetTitle(); return; } - + // make sure that we are returning to the login view. vm.view = "login"; vm.loginStates.submitButton = "busy"; userService.authenticate(vm.login, vm.password) - .then(function(data) { - vm.loginStates.submitButton = "success"; - userService._retryRequestQueue(true); - if (vm.onLogin) { - vm.onLogin(); - } - }, - function(reason) { + .then(function (data) { + loginSuccess(); + }, + function (reason) { //is Two Factor required? if (reason.status === 402) { @@ -249,13 +266,13 @@ //setup a watch for both of the model values changing, if they change // while the form is invalid, then revalidate them so that the form can // be submitted again. - vm.loginForm.username.$viewChangeListeners.push(function() { + vm.loginForm.username.$viewChangeListeners.push(function () { if (vm.loginForm.$invalid) { vm.loginForm.username.$setValidity('auth', true); vm.loginForm.password.$setValidity('auth', true); } }); - vm.loginForm.password.$viewChangeListeners.push(function() { + vm.loginForm.password.$viewChangeListeners.push(function () { if (vm.loginForm.$invalid) { vm.loginForm.username.$setValidity('auth', true); vm.loginForm.password.$setValidity('auth', true); @@ -460,7 +477,7 @@ case "2fa-login": title = "Two Factor Authentication"; break; - } + } $scope.$emit("$changeTitle", title); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js index 5e007a7ff4a7..a236e7f5acb6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js @@ -86,6 +86,7 @@ Use this directive to render an umbraco button. The directive can be used to gen bindings: { action: "&?", href: "@?", + hrefTarget: "@?", type: "@", buttonStyle: "@?", state: " 1) { // nodes with variants scope.currentUrls = _.filter(scope.node.urls, (url) => (scope.currentVariant.language && scope.currentVariant.language.culture === url.culture)); @@ -329,7 +329,7 @@ scope.currentUrls = scope.node.urls; } - // figure out if multiple cultures apply across the content urls + // figure out if multiple cultures apply across the content URLs scope.currentUrlsHaveMultipleCultures = _.keys(_.groupBy(scope.currentUrls, url => url.culture)).length > 1; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js index bb076e5bff3f..47ef460c53fc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js @@ -10,19 +10,18 @@ angular.module("umbraco.directives") } }; - var enabled = true; //check if there's a value for the attribute, if there is and it's false then we conditionally don't //use auto focus. if (attrs.umbAutoFocus) { attrs.$observe("umbAutoFocus", function (newVal) { - enabled = (newVal === "false" || newVal === 0 || newVal === false) ? false : true; + var enabled = (newVal === "false" || newVal === 0 || newVal === false) ? false : true; + if (enabled) { + $timeout(function() { + update(); + }); + } }); } - if (enabled) { - $timeout(function() { - update(); - }); - } }; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js index 792931627575..efbc384cb45a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js @@ -75,7 +75,8 @@ labelKey: "@?", onChange: "&?", autoFocus: "true to hide the label. +@param {string=} alias The alias of the field within the control group. +@param {string=} labelFor The alias of the field that the label is for, used for validation. +@param {boolean=} required Set to true to mark the field as required. + **/ + angular.module("umbraco.directives.html") .directive('umbControlGroup', function (localizationService) { return { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js index 2f682205ad84..1dcccda481c6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js @@ -14,10 +14,10 @@ The prompt can be opened in four direction up, down, left or right.

+ @@ -71,23 +71,17 @@ The prompt can be opened in four direction up, down, left or right.

function link(scope, el, attr, ctrl) { - scope.clickButton = function (event) { - if(scope.onDelete) { - scope.onDelete({$event: event}); - } - } + scope.clickConfirm = function() { + if(scope.onConfirm) { + scope.onConfirm(); + } + }; - scope.clickConfirm = function() { - if(scope.onConfirm) { - scope.onConfirm(); - } - }; - - scope.clickCancel = function() { - if(scope.onCancel) { - scope.onCancel(); - } - }; + scope.clickCancel = function() { + if(scope.onCancel) { + scope.onCancel(); + } + }; } @@ -97,8 +91,6 @@ The prompt can be opened in four direction up, down, left or right.

templateUrl: 'views/components/umb-confirm-action.html', scope: { direction: "@", - show: "<", - onDelete: "&?", onConfirm: "&", onCancel: "&" }, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js index 517776388b91..87d976f6d946 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js @@ -15,16 +15,7 @@ Simple icon Icon with additional attribute. It can be treated like any other dom element
-    
-
- -Manual svg string -This format is only used in the iconpicker.html -
-    
-    
+    
 
@example **/ @@ -43,13 +34,19 @@ This format is only used in the iconpicker.html svgString: "=?" }, - link: function (scope) { - + link: function (scope, element) { if (scope.svgString === undefined && scope.svgString !== null && scope.icon !== undefined && scope.icon !== null) { - var icon = scope.icon.split(" ")[0]; // Ensure that only the first part of the icon is used as sometimes the color is added too, e.g. see umbeditorheader.directive scope.openIconPicker + const observer = new IntersectionObserver(_lazyRequestIcon, {rootMargin: "100px"}); + const iconEl = element[0]; - _requestIcon(icon); + observer.observe(iconEl); + + // make sure to disconnect the observer when the scope is destroyed + scope.$on('$destroy', function () { + observer.disconnect(); + }); } + scope.$watch("icon", function (newValue, oldValue) { if (newValue && oldValue) { var newicon = newValue.split(" ")[0]; @@ -61,6 +58,17 @@ This format is only used in the iconpicker.html } }); + function _lazyRequestIcon(entries, observer) { + entries.forEach(entry => { + if (entry.isIntersecting === true) { + observer.disconnect(); + + var icon = scope.icon.split(" ")[0]; // Ensure that only the first part of the icon is used as sometimes the color is added too, e.g. see umbeditorheader.directive scope.openIconPicker + _requestIcon(icon); + } + }); + } + function _requestIcon(icon) { // Reset svg string before requesting new icon. scope.svgString = null; diff --git a/src/Umbraco.Web.UI.Client/src/common/interceptors/_module.js b/src/Umbraco.Web.UI.Client/src/common/interceptors/_module.js index 69a4fe35c915..6b2d993fd526 100644 --- a/src/Umbraco.Web.UI.Client/src/common/interceptors/_module.js +++ b/src/Umbraco.Web.UI.Client/src/common/interceptors/_module.js @@ -6,6 +6,7 @@ angular.module('umbraco.interceptors', []) $httpProvider.interceptors.push('securityInterceptor'); $httpProvider.interceptors.push('debugRequestInterceptor'); + $httpProvider.interceptors.push('requiredHeadersInterceptor'); $httpProvider.interceptors.push('doNotPostDollarVariablesOnPostRequestInterceptor'); $httpProvider.interceptors.push('cultureRequestInterceptor'); diff --git a/src/Umbraco.Web.UI.Client/src/common/interceptors/requiredheaders.interceptor.js b/src/Umbraco.Web.UI.Client/src/common/interceptors/requiredheaders.interceptor.js new file mode 100644 index 000000000000..3d96b86acd60 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/interceptors/requiredheaders.interceptor.js @@ -0,0 +1,28 @@ +(function () { + 'use strict'; + + /** + * Used to set required headers on all requests where necessary + * @param {any} $q + * @param {any} urlHelper + */ + function requiredHeadersInterceptor($q, urlHelper) { + return { + //dealing with requests: + 'request': function (config) { + + // This is a standard header that should be sent for all ajax requests and is required for + // how the server handles auth rejections, etc... see + // https://github.com/aspnet/AspNetKatana/blob/e2b18ec84ceab7ffa29d80d89429c9988ab40144/src/Microsoft.Owin.Security.Cookies/Provider/DefaultBehavior.cs + // https://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with-web-api-and-401-response-codes/ + config.headers["X-Requested-With"] = "XMLHttpRequest"; + + return config; + } + }; + } + + angular.module('umbraco.interceptors').factory('requiredHeadersInterceptor', requiredHeadersInterceptor); + + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js index dfa0eae2973a..12e1144acceb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js @@ -51,7 +51,7 @@ } else { // lets crawl through all properties of layout to make sure get captured all `contentUdi` and `settingsUdi` properties. var propType = typeof obj[k]; - if(propType === "object" || propType === "array") { + if(propType != null && (propType === "object" || propType === "array")) { replaceUdisOfObject(obj[k], propValue) } } @@ -65,7 +65,7 @@ function replaceRawBlockListUDIsResolver(value, propClearingMethod) { - if (typeof value === "object") { + if (value != null && typeof value === "object") { // we got an object, and it has these three props then we are most likely dealing with a Block Editor. if ((value.layout !== undefined && value.contentData !== undefined && value.settingsData !== undefined)) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 868b8baba757..77ed357c35b0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -425,7 +425,11 @@ if (self.scaffolds) { self.scaffolds.push(formatScaffoldData(scaffold)); } - })); + }).catch( + () => { + // Do nothing if we get an error. + } + )); }); return $q.all(tasks); @@ -439,7 +443,14 @@ * @return {Array} array of strings representing alias. */ getAvailableAliasesForBlockContent: function () { - return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey).contentTypeAlias); + return this.blockConfigurations.map( + (blockConfiguration) => { + var scaffold = this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey); + if (scaffold) { + return scaffold.contentTypeAlias; + } + } + ); }, /** @@ -519,7 +530,7 @@ var dataModel = getDataByUdi(contentUdi, this.value.contentData); if (dataModel === null) { - console.error("Couldn't find content model of " + contentUdi) + console.error("Couldn't find content data of " + contentUdi) return null; } @@ -591,7 +602,7 @@ var settingsData = getDataByUdi(settingsUdi, this.value.settingsData); if (settingsData === null) { - console.error("Couldnt find content settings data of " + settingsUdi) + console.error("Couldnt find settings data of " + settingsUdi) return null; } 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 new file mode 100644 index 000000000000..1d2048b2f5e8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/externallogininfo.service.js @@ -0,0 +1,67 @@ +/** + * @ngdoc service + * @name umbraco.services.externalLoginInfoService + * @description A service for working with external login providers + **/ +function externalLoginInfoService(externalLoginInfo, umbRequestHelper) { + + 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.UmbracoBackOfficeExternalLoginOptions && provider.properties.UmbracoBackOfficeExternalLoginOptions.CustomBackOfficeView) { + return umbRequestHelper.convertVirtualToAbsolutePath(provider.properties.UmbracoBackOfficeExternalLoginOptions.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.UmbracoBackOfficeExternalLoginOptions.DenyLocalLogin === true); + } + else { + return provider.properties.UmbracoBackOfficeExternalLoginOptions.DenyLocalLogin; + } + } + + /** + * Returns all login providers + */ + function getLoginProviders() { + return externalLoginInfo.providers; + } + + /** 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.ExternalSignInAutoLinkOptions.AllowManualLinking; + } + }); + 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.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 731cc5ed368c..8df5a9ce8c56 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -464,7 +464,7 @@ function serverValidationManager($timeout) { * @ngdoc function * @name addErrorsForModelState * @methodOf umbraco.services.serverValidationManager - * @param {any} modelState + * @param {any} modelState the modelState object * @param {any} parentValidationPath optional parameter specifying a nested element's UDI for which this property belongs (for complex editors) * @description * This wires up all of the server validation model state so that valServer and valServerField directives work diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index de6fbaf782fb..00871caab1b3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -1,5 +1,5 @@ angular.module('umbraco.services') - .factory('userService', function ($rootScope, eventsService, $q, $location, requestRetryQueue, authResource, emailMarketingResource, $timeout, angularHelper) { + .factory('userService', function ($rootScope, eventsService, $q, $location, $window, requestRetryQueue, authResource, emailMarketingResource, $timeout, angularHelper) { var currentUser = null; var lastUserId = null; @@ -166,7 +166,7 @@ angular.module('umbraco.services') }, /** Internal method to retry all request after sucessfull login */ - _retryRequestQueue: function(success) { + _retryRequestQueue: function (success) { retryRequestQueue(success) }, @@ -185,18 +185,22 @@ angular.module('umbraco.services') authenticate: function (login, password) { return authResource.performLogin(login, password) - .then(function(data) { + .then(function (data) { // Check if user has a start node set. - if(data.startContentIds.length === 0 && data.startMediaIds.length === 0){ + if (data.startContentIds.length === 0 && data.startMediaIds.length === 0) { var errorMsg = "User has no start-nodes"; var result = { errorMsg: errorMsg, user: data, authenticated: false, lastUserId: lastUserId, loginType: "credentials" }; eventsService.emit("app.notAuthenticated", result); + // TODO: How does this make sense? How can you throw from a promise? Does this get caught by the rejection? + // If so then return $q.reject should be used. throw result; } - + return data; - + + }, function (err) { + return $q.reject(err); }).then(this.setAuthenticationSuccessful); }, setAuthenticationSuccessful: function (data) { @@ -218,8 +222,14 @@ angular.module('umbraco.services') return authResource.performLogout() .then(function (data) { userAuthExpired(); - //done! - return null; + + if (data && data.signOutRedirectUrl) { + $window.location.replace(data.signOutRedirectUrl); + } + else { + //done! + return null; + } }); }, @@ -235,9 +245,9 @@ angular.module('umbraco.services') setCurrentUser(data); deferred.resolve(currentUser); - }, function () { + }, function (err) { //it failed, so they are not logged in - deferred.reject(); + deferred.reject(err); }); return deferred.promise; @@ -245,7 +255,7 @@ angular.module('umbraco.services') /** Returns the current user object in a promise */ getCurrentUser: function (args) { - + if (!currentUser) { return authResource.getCurrentUser() .then(function (data) { @@ -260,9 +270,9 @@ angular.module('umbraco.services') setCurrentUser(data); return $q.when(currentUser); - }, function () { + }, function (err) { //it failed, so they are not logged in - return $q.reject(currentUser); + return $q.reject(err); }); } diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 01e595ac24d3..359c3dd427ce 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -177,6 +177,7 @@ @import "components/umb-range-slider.less"; @import "components/umb-number.less"; @import "components/umb-tags-editor.less"; +@import "components/umb-search-filter.less"; @import "components/buttons/umb-button.less"; @import "components/buttons/umb-button-group.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/card.less b/src/Umbraco.Web.UI.Client/src/less/components/card.less index 017468fa0ce6..a1a4b4bc5ed9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/card.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/card.less @@ -159,6 +159,7 @@ justify-content: center; flex-direction: column; background-color: transparent; + word-break: break-word; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less index d79bbbde202c..f11630b36340 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less @@ -8,45 +8,88 @@ border-radius: 100px; } +.umb-badge__count { + display: flex; + width: 1rem; + height: 1rem; + line-height: 1; + justify-content: center; + align-items: center; + border-radius: 50%; + font-size: 12px; +} + // Colors .umb-badge--primary { background-color: @blueDark; color: @white; + + .umb-badge__count { + background-color: darken(@blueDark, 5%); + } } .umb-badge--secondary { background-color: @blueExtraDark; color: @white; + + .umb-badge__count { + background-color: darken(@blueExtraDark, 8%); + } } .umb-badge--gray { background-color: @sand-2; color: @blueDark; + + .umb-badge__count { + background-color: darken(@sand-2, 8%); + } } .umb-badge--danger { background-color: @red; color: @white; + + .umb-badge__count { + background-color: darken(@red, 8%); + } } .umb-badge--info { background-color: @blue; color: @white; + + .umb-badge__count { + background-color: darken(@blue, 8%); + } } .umb-badge--warning { background-color: @orange; color: @white; + + .umb-badge__count { + background-color: darken(@orange, 8%); + } } .umb-badge--success { background-color: @green; color: @white; + + .umb-badge__count { + background-color: darken(@green, 8%); + } } .umb-badge--dark { background-color: @grayDark; color: @white; + + .umb-badge__count { + background-color: darken(@grayDark, 8%); + } } // Size diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-confirm-action.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-confirm-action.less index a6548123ac7f..112194f012ee 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-confirm-action.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-confirm-action.less @@ -1,8 +1,3 @@ -//WRAPPER -.umb_confirm-action { - display: inline-block; -} - // OVERLAY .umb_confirm-action__overlay { position: absolute; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index cff9980483e4..9567840049f9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -97,7 +97,8 @@ height: 20px; width: 20px; position: absolute; - top: -1px; + top: 0; + } &__check { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less new file mode 100644 index 000000000000..bda9fa7a7e88 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less @@ -0,0 +1,29 @@ +html .umb-search-filter { + position: relative; + height: 30px; + width: 190px; + + &.w-100 { + width: 100%; + } + + &.mb-15 { + margin-bottom: 15px; + } + + &__input { + padding-left: 30px; + padding-right: 6px; + width: inherit; + margin: 0; + } + + .icon-search { + color: #d8d7d9; + position: absolute; + top: 0; + bottom: 0; + left: 10px; + margin: auto 0; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/fonts.less b/src/Umbraco.Web.UI.Client/src/less/fonts.less index 73bc36037f80..e9a84656f9ec 100644 --- a/src/Umbraco.Web.UI.Client/src/less/fonts.less +++ b/src/Umbraco.Web.UI.Client/src/less/fonts.less @@ -15,8 +15,7 @@ font-family: 'Lato'; src: local('LatoLatin Black'), local('LatoLatin-Black'), - url('@{latoPath}/LatoLatin-Black.woff2') format('woff2'), /* Super Modern Browsers */ - url('@{latoPath}/LatoLatin-Black.woff') format('woff'); /* Modern Browsers */ + url('@{latoPath}/LatoLatin-Black.woff2') format('woff2'); font-style: normal; font-display: swap; font-weight: 900; @@ -28,8 +27,7 @@ font-family: 'Lato'; src: local('LatoLatin BlackItalic'), local('LatoLatin-BlackItalic'), - url('@{latoPath}/LatoLatin-BlackItalic.woff2') format('woff2'), /* Super Modern Browsers */ - url('@{latoPath}/LatoLatin-BlackItalic.woff') format('woff'); /* Modern Browsers */ + url('@{latoPath}/LatoLatin-BlackItalic.woff2') format('woff2'); font-style: italic; font-weight: 900; font-display: swap; @@ -41,8 +39,7 @@ font-family: 'Lato'; src: local('LatoLatin Bold'), local('LatoLatin-Bold'), - url('@{latoPath}/LatoLatin-Bold.woff2') format('woff2'), /* Super Modern Browsers */ - url('@{latoPath}/LatoLatin-Bold.woff') format('woff'); /* Modern Browsers */ + url('@{latoPath}/LatoLatin-Bold.woff2') format('woff2'); font-style: normal; font-weight: 700; font-display: swap; @@ -54,8 +51,7 @@ font-family: 'Lato'; src: local('LatoLatin BoldItalic'), local('LatoLatin-BoldItalic'), - url('@{latoPath}/LatoLatin-BoldItalic.woff2') format('woff2'), /* Super Modern Browsers */ - url('@{latoPath}/LatoLatin-BoldItalic.woff') format('woff'); /* Modern Browsers */ + url('@{latoPath}/LatoLatin-BoldItalic.woff2') format('woff2'); font-style: italic; font-weight: 700; font-display: swap; @@ -67,8 +63,7 @@ font-family: 'Lato'; src: local('LatoLatin Italic'), local('LatoLatin-Italic'), - url('@{latoPath}/LatoLatin-Italic.woff2') format('woff2'), /* Super Modern Browsers */ - url('@{latoPath}/LatoLatin-Italic.woff') format('woff'); /* Modern Browsers */ + url('@{latoPath}/LatoLatin-Italic.woff2') format('woff2'); font-style: italic; font-weight: 400; font-display: swap; @@ -80,8 +75,7 @@ font-family: 'Lato'; src: local('LatoLatin Regular'), local('LatoLatin-Regular'), - url('@{latoPath}/LatoLatin-Regular.woff2') format('woff2'), /* Super Modern Browsers */ - url('@{latoPath}/LatoLatin-Regular.woff') format('woff'); /* Modern Browsers */ + url('@{latoPath}/LatoLatin-Regular.woff2') format('woff2'); font-style: normal; font-weight: 400; font-display: swap; @@ -93,8 +87,7 @@ font-family: 'Lato'; src: local('LatoLatin Light'), local('LatoLatin-Light'), - url('@{latoPath}/LatoLatin-Light.woff2') format('woff2'), /* Super Modern Browsers */ - url('@{latoPath}/LatoLatin-Light.woff') format('woff'); /* Modern Browsers */ + url('@{latoPath}/LatoLatin-Light.woff2') format('woff2'); font-style: normal; font-weight: 300; font-display: swap; @@ -106,8 +99,7 @@ font-family: 'Lato'; src: local('LatoLatin LightItalic'), local('LatoLatin-LightItalic'), - url('@{latoPath}/LatoLatin-LightItalic.woff2') format('woff2'), /* Super Modern Browsers */ - url('@{latoPath}/LatoLatin-LightItalic.woff') format('woff'); /* Modern Browsers */ + url('@{latoPath}/LatoLatin-LightItalic.woff2') format('woff2'); font-style: italic; font-weight: 300; font-display: swap; diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 3bf00fb25ce0..e1e368f2e2c8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -208,6 +208,13 @@ umb-property:last-of-type .umb-control-group { .control-description { display: block; clear: both; + overflow-wrap: break-word; + } + + &::after { + content: ''; + display: block; + clear: both; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html index 6dea4debb61d..4b08d4e5fce6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html @@ -16,7 +16,7 @@
-
/// [SetAngularAntiForgeryTokens] + [DenyLocalLoginAuthorization] public async Task PostRequestPasswordReset(RequestPasswordResetModel model) { // If this feature is switched off in configuration the UI will be amended to not make the request to reset password available. @@ -476,7 +504,12 @@ public HttpResponseMessage PostLogout() if (UserManager != null) { int.TryParse(User.Identity.GetUserId(), out var userId); - UserManager.RaiseLogoutSuccessEvent(userId); + var args = UserManager.RaiseLogoutSuccessEvent(userId); + if (!args.SignOutRedirectUrl.IsNullOrWhiteSpace()) + return Request.CreateResponse(new + { + signOutRedirectUrl = args.SignOutRedirectUrl + }); } return Request.CreateResponse(HttpStatusCode.OK); diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index cd5a3a50b658..18740d41fc60 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.Web; using System.Web.Mvc; using System.Web.UI; using Microsoft.AspNet.Identity; @@ -113,7 +112,7 @@ public async Task VerifyInvite(string invite) //if you are hitting VerifyInvite, you're already signed in as a different user, and the token is invalid //you'll exit on one of the return RedirectToAction("Default") but you're still logged in so you just get //dumped at the default admin view with no detail - if(Security.IsAuthenticated()) + if (Security.IsAuthenticated()) { AuthenticationManager.SignOut( Core.Constants.Security.BackOfficeAuthenticationType, @@ -213,7 +212,7 @@ public JsonNetResult LocalizedText(string culture = null) { var slashIndex = kv.Key.IndexOf('/'); var areaAlias = kv.Key.Substring(0, slashIndex); - var valueAlias = kv.Key.Substring(slashIndex+1); + var valueAlias = kv.Key.Substring(slashIndex + 1); return new { areaAlias, @@ -333,7 +332,7 @@ public ActionResult LinkLogin(string provider) } [HttpGet] - public async Task ValidatePasswordResetCode([Bind(Prefix = "u")]int userId, [Bind(Prefix = "r")]string resetCode) + public async Task ValidatePasswordResetCode([Bind(Prefix = "u")] int userId, [Bind(Prefix = "r")] string resetCode) { var user = UserManager.FindById(userId); if (user != null) @@ -391,8 +390,9 @@ private async Task RenderDefaultOrProcessExternalLoginAsync( ViewData.SetUmbracoPath(GlobalSettings.GetUmbracoMvcArea()); - //check if there is the TempData with the any token name specified, if so, assign to view bag and render the view - if (ViewData.FromTempData(TempData, ViewDataExtensions.TokenExternalSignInError) || + //check if there is the TempData or cookies with the any token name specified, if so, assign to view bag and render the view + if (ViewData.FromBase64CookieData(HttpContext, ViewDataExtensions.TokenExternalSignInError) || + ViewData.FromTempData(TempData, ViewDataExtensions.TokenExternalSignInError) || ViewData.FromTempData(TempData, ViewDataExtensions.TokenPasswordResetCode)) return defaultResponse(); @@ -402,6 +402,17 @@ private async Task RenderDefaultOrProcessExternalLoginAsync( if (loginInfo == null || loginInfo.ExternalIdentity.IsAuthenticated == false) { + + // if the user is not logged in, check if there's any auto login redirects specified + if (UmbracoContext.Security.ValidateCurrentUser(false) != ValidateRequestAttempt.Success) + { + var oauthRedirectAuthProvider = OwinContext.Authentication.GetAutoLoginProvider(); + if (!oauthRedirectAuthProvider.IsNullOrWhiteSpace()) + { + return ExternalLogin(oauthRedirectAuthProvider); + } + } + return defaultResponse(); } @@ -415,7 +426,7 @@ private async Task ExternalSignInAsync(ExternalLoginInfo loginInfo if (response == null) throw new ArgumentNullException("response"); ExternalSignInAutoLinkOptions autoLinkOptions = null; - //Here we can check if the provider associated with the request has been configured to allow + // Here we can check if the provider associated with the request has been configured to allow // new users (auto-linked external accounts). This would never be used with public providers such as // Google, unless you for some reason wanted anybody to be able to access the backend if they have a Google account // .... not likely! @@ -426,19 +437,13 @@ private async Task ExternalSignInAsync(ExternalLoginInfo loginInfo } else { - autoLinkOptions = authType.GetExternalAuthenticationOptions(); + autoLinkOptions = authType.GetExternalSignInAutoLinkOptions(); } // Sign in the user with this external login provider if the user already has a login var user = await UserManager.FindAsync(loginInfo.Login); if (user != null) { - // TODO: It might be worth keeping some of the claims associated with the ExternalLoginInfo, in which case we - // wouldn't necessarily sign the user in here with the standard login, instead we'd update the - // UseUmbracoBackOfficeExternalCookieAuthentication extension method to have the correct provider and claims factory, - // ticket format, etc.. to create our back office user including the claims assigned and in this method we'd just ensure - // that the ticket is created and stored and that the user is logged in. - var shouldSignIn = true; if (autoLinkOptions != null && autoLinkOptions.OnExternalLogin != null) { @@ -459,7 +464,10 @@ private async Task ExternalSignInAsync(ExternalLoginInfo loginInfo { if (await AutoLinkAndSignInExternalAccount(loginInfo, autoLinkOptions) == false) { - ViewData.SetExternalSignInError(new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to an account" }); + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo.Login.LoginProvider, + new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to an account" })); } //Remove the cookie otherwise this message will keep appearing @@ -483,15 +491,34 @@ private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo logi //we are allowing auto-linking/creating of local accounts if (loginInfo.Email.IsNullOrWhiteSpace()) { - ViewData.SetExternalSignInError(new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not provided an email address, the account cannot be linked." }); + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo.Login.LoginProvider, + new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not provided an email address, the account cannot be linked." })); } else { //Now we need to perform the auto-link, so first we need to lookup/create a user with the email address - var foundByEmail = Services.UserService.GetByEmail(loginInfo.Email); - if (foundByEmail != null) + var autoLinkUser = UserManager.FindByEmail(loginInfo.Email); + if (autoLinkUser != null) { - ViewData.SetExternalSignInError(new[] { "A user with this email address already exists locally. You will need to login locally to Umbraco and link this external provider: " + loginInfo.Login.LoginProvider }); + try + { + //call the callback if one is assigned + autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo); + } + catch (Exception ex) + { + var msg = "Could not link login provider " + loginInfo.Login.LoginProvider + "."; + Logger.Error(ex, msg); + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo.Login.LoginProvider, + new[] { msg + " " + ex.Message })); + return true; + } + + await LinkUser(autoLinkUser, loginInfo); } else { @@ -500,7 +527,7 @@ private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo logi var groups = Services.UserService.GetUserGroupsByAlias(autoLinkOptions.GetDefaultUserGroups(UmbracoContext, loginInfo)); - var autoLinkUser = BackOfficeIdentityUser.CreateNew( + autoLinkUser = BackOfficeIdentityUser.CreateNew( loginInfo.Email, loginInfo.Email, autoLinkOptions.GetDefaultCulture(UmbracoContext, loginInfo)); @@ -511,37 +538,33 @@ private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo logi } //call the callback if one is assigned - if (autoLinkOptions.OnAutoLinking != null) + try + { + autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo); + } + catch (Exception ex) { - autoLinkOptions.OnAutoLinking(autoLinkUser, loginInfo); + var msg = "Could not link login provider " + loginInfo.Login.LoginProvider + "."; + Logger.Error(ex, msg); + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo.Login.LoginProvider, + new[] { msg + " " + ex.Message })); + return true; } var userCreationResult = await UserManager.CreateAsync(autoLinkUser); if (userCreationResult.Succeeded == false) { - ViewData.SetExternalSignInError(userCreationResult.Errors); + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo.Login.LoginProvider, + userCreationResult.Errors)); } else { - var linkResult = await UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login); - if (linkResult.Succeeded == false) - { - ViewData.SetExternalSignInError(linkResult.Errors); - - //If this fails, we should really delete the user since it will be in an inconsistent state! - var deleteResult = await UserManager.DeleteAsync(autoLinkUser); - if (deleteResult.Succeeded == false) - { - //DOH! ... this isn't good, combine all errors to be shown - ViewData.SetExternalSignInError(linkResult.Errors.Concat(deleteResult.Errors)); - } - } - else - { - //sign in - await SignInManager.SignInAsync(autoLinkUser, isPersistent: false, rememberBrowser: false); - } + await LinkUser(autoLinkUser, loginInfo); } } @@ -549,60 +572,56 @@ private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo logi return true; } - private ActionResult RedirectToLocal(string returnUrl) + private async Task LinkUser(BackOfficeIdentityUser autoLinkUser, ExternalLoginInfo loginInfo) { - if (Url.IsLocalUrl(returnUrl)) + var existingLogins = await UserManager.GetLoginsAsync(autoLinkUser.Id); + var exists = existingLogins.FirstOrDefault(x => x.LoginProvider == loginInfo.Login.LoginProvider && x.ProviderKey == loginInfo.Login.ProviderKey); + + // if it already exists (perhaps it was added in the AutoLink callbak) then we just continue + if (exists != null) { - return Redirect(returnUrl); + //sign in + await SignInManager.SignInAsync(autoLinkUser, isPersistent: false, rememberBrowser: false); + return; } - return Redirect("/"); - } - - // Used for XSRF protection when adding external logins - private const string XsrfKey = "XsrfId"; - private class ChallengeResult : HttpUnauthorizedResult - { - public ChallengeResult(string provider, string redirectUri, string userId = null) + var linkResult = await UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login); + if (linkResult.Succeeded) { - LoginProvider = provider; - RedirectUri = redirectUri; - UserId = userId; + //we're good! sign in + await SignInManager.SignInAsync(autoLinkUser, isPersistent: false, rememberBrowser: false); + return; } - private string LoginProvider { get; set; } - private string RedirectUri { get; set; } - private string UserId { get; set; } + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo.Login.LoginProvider, + linkResult.Errors)); - public override void ExecuteResult(ControllerContext context) + //If this fails, we should really delete the user since it will be in an inconsistent state! + var deleteResult = await UserManager.DeleteAsync(autoLinkUser); + if (!deleteResult.Succeeded) { - //Ensure the forms auth module doesn't do a redirect! - context.HttpContext.Response.SuppressFormsAuthenticationRedirect = true; - - var owinCtx = context.HttpContext.GetOwinContext(); - - //First, see if a custom challenge result callback is specified for the provider - // and use it instead of the default if one is supplied. - var loginProvider = owinCtx.Authentication - .GetExternalAuthenticationTypes() - .FirstOrDefault(p => p.AuthenticationType == LoginProvider); - if (loginProvider != null) - { - var providerChallengeResult = loginProvider.GetSignInChallengeResult(owinCtx); - if (providerChallengeResult != null) - { - owinCtx.Authentication.Challenge(providerChallengeResult, LoginProvider); - return; - } - } + //DOH! ... this isn't good, combine all errors to be shown + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo.Login.LoginProvider, + linkResult.Errors.Concat(deleteResult.Errors))); + } + } - var properties = new AuthenticationProperties() { RedirectUri = RedirectUri.EnsureEndsWith('/') }; - if (UserId != null) - { - properties.Dictionary[XsrfKey] = UserId; - } - owinCtx.Authentication.Challenge(properties, LoginProvider); + private ActionResult RedirectToLocal(string returnUrl) + { + if (Url.IsLocalUrl(returnUrl)) + { + return Redirect(returnUrl); } + return Redirect("/"); } + + // Used for XSRF protection when adding external logins + public const string XsrfKey = "XsrfId"; + } + } diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 60d3a6f77941..7dfdd022e308 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -22,6 +22,7 @@ using Umbraco.Web.Mvc; using Umbraco.Web.Profiling; using Umbraco.Web.PropertyEditors; +using Umbraco.Web.Security; using Umbraco.Web.Trees; using Constants = Umbraco.Core.Constants; @@ -109,7 +110,7 @@ internal Dictionary GetServerVariables() { // TODO: Add 'umbracoApiControllerBaseUrl' which people can use in JS // to prepend their URL. We could then also use this in our own resources instead of - // having each url defined here explicitly - we can do that in v8! for now + // having each URL defined here explicitly - we can do that in v8! for now // for umbraco services we'll stick to explicitly defining the endpoints. {"externalLoginsUrl", _urlHelper.Action("ExternalLogin", "BackOffice")}, @@ -379,8 +380,7 @@ internal Dictionary GetServerVariables() "externalLogins", new Dictionary { { - "providers", _owinContext.Authentication.GetExternalAuthenticationTypes() - .Where(p => p.Properties.ContainsKey("UmbracoBackOffice")) + "providers", _owinContext.Authentication.GetBackOfficeExternalLoginProviders() .Select(p => new { authType = p.AuthenticationType, caption = p.Caption, diff --git a/src/Umbraco.Web/Editors/ChallengeResult.cs b/src/Umbraco.Web/Editors/ChallengeResult.cs new file mode 100644 index 000000000000..00c0aa187d5c --- /dev/null +++ b/src/Umbraco.Web/Editors/ChallengeResult.cs @@ -0,0 +1,54 @@ +using System.Linq; +using System.Web; +using System.Web.Mvc; +using Microsoft.Owin.Security; +using Umbraco.Core; +using Umbraco.Web.Security; + +namespace Umbraco.Web.Editors +{ + public class ChallengeResult : HttpUnauthorizedResult + { + public ChallengeResult(string provider, string redirectUri, string userId = null) + { + LoginProvider = provider; + RedirectUri = redirectUri; + UserId = userId; + } + + private string LoginProvider { get; set; } + private string RedirectUri { get; set; } + private string UserId { get; set; } + + public override void ExecuteResult(ControllerContext context) + { + //Ensure the forms auth module doesn't do a redirect! + context.HttpContext.Response.SuppressFormsAuthenticationRedirect = true; + + var owinCtx = context.HttpContext.GetOwinContext(); + + //First, see if a custom challenge result callback is specified for the provider + // and use it instead of the default if one is supplied. + var loginProvider = owinCtx.Authentication + .GetExternalAuthenticationTypes() + .FirstOrDefault(p => p.AuthenticationType == LoginProvider); + if (loginProvider != null) + { + var providerChallengeResult = loginProvider.GetSignInChallengeResult(owinCtx); + if (providerChallengeResult != null) + { + owinCtx.Authentication.Challenge(providerChallengeResult, LoginProvider); + return; + } + } + + var properties = new AuthenticationProperties() { RedirectUri = RedirectUri.EnsureEndsWith('/') }; + if (UserId != null) + { + properties.Dictionary[BackOfficeController.XsrfKey] = UserId; + } + owinCtx.Authentication.Challenge(properties, LoginProvider); + } + } + +} diff --git a/src/Umbraco.Web/Editors/CodeFileController.cs b/src/Umbraco.Web/Editors/CodeFileController.cs index 5ddbb506c965..0e1c4b3e607e 100644 --- a/src/Umbraco.Web/Editors/CodeFileController.cs +++ b/src/Umbraco.Web/Editors/CodeFileController.cs @@ -139,7 +139,7 @@ public HttpResponseMessage PostCreateContainer(string type, string parentId, str /// Used to get a specific file from disk via the FileService ///
/// This is a string but will be 'scripts' 'partialViews', 'partialViewMacros' or 'stylesheets' - /// The filename or urlencoded path of the file to open + /// The filename or URL encoded path of the file to open /// The file and its contents from the virtualPath public CodeFileDisplay GetByPath(string type, string virtualPath) { @@ -291,7 +291,7 @@ public CodeFileDisplay GetScaffold(string type, string id, string snippetName = /// Used to delete a specific file from disk via the FileService /// /// This is a string but will be 'scripts' 'partialViews', 'partialViewMacros' or 'stylesheets' - /// The filename or urlencoded path of the file to delete + /// The filename or URL encoded path of the file to delete /// Will return a simple 200 if file deletion succeeds [HttpDelete] [HttpPost] diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 3938ae5ab874..65d930590678 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -207,7 +207,7 @@ public IEnumerable GetPath(Udi id, UmbracoEntityTypes type) } /// - /// Gets the url of an entity + /// Gets the URL of an entity /// /// UDI of the entity to fetch URL for /// The culture to fetch the URL for @@ -236,7 +236,7 @@ public HttpResponseMessage GetUrl(Udi udi, string culture = "*") } /// - /// Gets the url of an entity + /// Gets the URL of an entity /// /// Int id of the entity to fetch URL for /// The type of entity such as Document, Media, Member diff --git a/src/Umbraco.Web/Editors/Filters/DenyLocalLoginAuthorizationAttribute.cs b/src/Umbraco.Web/Editors/Filters/DenyLocalLoginAuthorizationAttribute.cs new file mode 100644 index 000000000000..89a67d8f78de --- /dev/null +++ b/src/Umbraco.Web/Editors/Filters/DenyLocalLoginAuthorizationAttribute.cs @@ -0,0 +1,18 @@ +using System.Web.Http; +using System.Web.Http.Controllers; +using Umbraco.Web.WebApi; +using Umbraco.Web.Security; + +namespace Umbraco.Web.Editors.Filters +{ + internal class DenyLocalLoginAuthorizationAttribute : AuthorizeAttribute + { + protected override bool IsAuthorized(HttpActionContext actionContext) + { + var owinContext = actionContext.Request.TryGetOwinContext().Result; + + // no authorization if any external logins deny local login + return !owinContext.Authentication.HasDenyLocalLogin(); + } + } +} diff --git a/src/Umbraco.Web/Editors/Filters/IsCurrentUserModelFilterAttribute.cs b/src/Umbraco.Web/Editors/Filters/IsCurrentUserModelFilterAttribute.cs index b9c6eb45eb2b..61122e2b6e07 100644 --- a/src/Umbraco.Web/Editors/Filters/IsCurrentUserModelFilterAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/IsCurrentUserModelFilterAttribute.cs @@ -22,9 +22,9 @@ public override void OnActionExecuted(HttpActionExecutedContext actionExecutedCo if (objectContent != null) { var model = objectContent.Value as UserBasic; - if (model != null) + if (model != null && model.Id is int userId) { - model.IsCurrentUser = (int) model.Id == user.Id; + model.IsCurrentUser = userId == user.Id; } else { @@ -33,7 +33,10 @@ public override void OnActionExecuted(HttpActionExecutedContext actionExecutedCo { foreach (var userBasic in collection) { - userBasic.IsCurrentUser = (int) userBasic.Id == user.Id; + if (userBasic.Id is int uid) + { + userBasic.IsCurrentUser = uid == user.Id; + } } } else @@ -43,7 +46,10 @@ public override void OnActionExecuted(HttpActionExecutedContext actionExecutedCo { foreach (var userBasic in paged.Items) { - userBasic.IsCurrentUser = (int)userBasic.Id == user.Id; + if (userBasic.Id is int uid) + { + userBasic.IsCurrentUser = uid == user.Id; + } } } } diff --git a/src/Umbraco.Web/Editors/PreviewController.cs b/src/Umbraco.Web/Editors/PreviewController.cs index f148655acd3e..d8979e75b206 100644 --- a/src/Umbraco.Web/Editors/PreviewController.cs +++ b/src/Umbraco.Web/Editors/PreviewController.cs @@ -107,7 +107,7 @@ public ActionResult Frame(int id, string culture) Response.Cookies.Set(new HttpCookie(Constants.Web.PreviewCookieName, previewToken)); - // use a numeric url because content may not be in cache and so .Url would fail + // use a numeric URL because content may not be in cache and so .Url would fail var query = culture.IsNullOrWhiteSpace() ? string.Empty : $"?culture={culture}"; Response.Redirect($"../../{id}.aspx{query}", true); diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 76c4228a016c..7bb709d72ced 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -29,6 +29,7 @@ using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; +using Umbraco.Web.Security; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; @@ -49,7 +50,7 @@ public UsersController(IGlobalSettings globalSettings, IUmbracoContextAccessor u } /// - /// Returns a list of the sizes of gravatar urls for the user or null if the gravatar server cannot be reached + /// Returns a list of the sizes of gravatar URLs for the user or null if the gravatar server cannot be reached /// /// public string[] GetCurrentUserAvatarUrls() @@ -373,12 +374,6 @@ public async Task PostInviteUser(UserInvite userSave) throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); } - if (EmailSender.CanSendRequiredEmail == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse("No Email server is configured")); - } - IUser user; if (Current.Configs.Settings().Security.UsernameIsEmail) { @@ -388,9 +383,17 @@ public async Task PostInviteUser(UserInvite userSave) else { //first validate the username if we're showing it - user = CheckUniqueUsername(userSave.Username, u => u.LastLoginDate != default(DateTime) || u.EmailConfirmedDate.HasValue); + user = CheckUniqueUsername(userSave.Username, u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue); + } + user = CheckUniqueEmail(userSave.Email, u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue); + + var userMgr = TryGetOwinContext().Result.GetBackOfficeUserManager(); + + if (!EmailSender.CanSendRequiredEmail && !userMgr.HasSendingUserInviteEventHandler) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse("No Email server is configured")); } - user = CheckUniqueEmail(userSave.Email, u => u.LastLoginDate != default(DateTime) || u.EmailConfirmedDate.HasValue); //Perform authorization here to see if the current user can actually save this user with the info being requested var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService); @@ -424,16 +427,48 @@ public async Task PostInviteUser(UserInvite userSave) //ensure the invited date is set user.InvitedDate = DateTime.Now; - //Save the updated user + //Save the updated user (which will process the user groups too) Services.UserService.Save(user); var display = Mapper.Map(user); - //send the email + var inviteArgs = new UserInviteEventArgs( + Request.TryGetHttpContext().Result.GetCurrentRequestIpAddress(), + performingUser: Security.GetUserId().Result, + userSave, + user); - await SendUserInviteEmailAsync(display, Security.CurrentUser.Name, Security.CurrentUser.Email, user, userSave.Message); + try + { + userMgr.RaiseSendingUserInvite(inviteArgs); + } + catch (Exception ex) + { + Logger.Error(ex, "An error occured in a custom event handler while inviting the user"); + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse($"An error occured inviting the user (check logs for more info): {ex.Message}")); + } - display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/resendInviteHeader"), Services.TextService.Localize("speechBubbles/resendInviteSuccess", new[] { user.Name })); + // If the event is handled then no need to send the email + if (inviteArgs.InviteHandled) + { + // if no user result was created then map the minimum args manually for the UI + if (!inviteArgs.ShowUserResult) + { + display = new UserDisplay + { + Name = userSave.Name, + Email = userSave.Email, + Username = userSave.Username + }; + } + } + else + { + //send the email + await SendUserInviteEmailAsync(display, Security.CurrentUser.Name, Security.CurrentUser.Email, user, userSave.Message); + } + display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/resendInviteHeader"), Services.TextService.Localize("speechBubbles/resendInviteSuccess", new[] { user.Name })); return display; } @@ -478,7 +513,7 @@ private async Task SendUserInviteEmailAsync(UserBasic userDisplay, string from, WebUtility.UrlEncode("|"), token.ToUrlBase64()); - // Get an mvc helper to get the url + // Get an mvc helper to get the URL var http = EnsureHttpContext(); var urlHelper = new UrlHelper(http.Request.RequestContext); var action = urlHelper.Action("VerifyInvite", "BackOffice", @@ -518,9 +553,9 @@ await UserManager.EmailService.SendAsync( /// /// [OutgoingEditorModelEvent] - public async Task PostSaveUser(UserSave userSave) + public UserDisplay PostSaveUser(UserSave userSave) { - if (userSave == null) throw new ArgumentNullException("userSave"); + if (userSave == null) throw new ArgumentNullException(nameof(userSave)); if (ModelState.IsValid == false) { @@ -545,6 +580,14 @@ public async Task PostSaveUser(UserSave userSave) var hasErrors = false; + // we need to check if there's any Deny Local login providers present, if so we need to ensure that the user's email address cannot be changed + var owinContext = Request.TryGetOwinContext().Result; + var hasDenyLocalLogin = owinContext.Authentication.HasDenyLocalLogin(); + if (hasDenyLocalLogin) + { + userSave.Email = found.Email; // it cannot change, this would only happen if people are mucking around with the request + } + var existing = Services.UserService.GetByEmail(userSave.Email); if (existing != null && existing.Id != userSave.Id) { diff --git a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs index 1d518fa1d391..466d6830c0da 100644 --- a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs @@ -9,11 +9,14 @@ using Umbraco.Web.Editors; using Umbraco.Web.Features; using Umbraco.Web.Models; +using Umbraco.Core; namespace Umbraco.Web { using Core.Configuration; + using System; using Umbraco.Web.JavaScript; + using Umbraco.Web.Security; /// /// HtmlHelper extensions for the back office @@ -26,7 +29,7 @@ public static class HtmlHelperBackOfficeExtensions /// /// /// - /// The post url used to sign in with external logins - this can change depending on for what service the external login is service. + /// The post URL used to sign in with external logins - this can change depending on for what service the external login is service. /// Example: normal back office login or authenticating upgrade login /// /// @@ -56,10 +59,9 @@ public static IHtmlString BareMinimumServerVariablesScript(this HtmlHelper html, /// /// /// - public static IHtmlString AngularValueExternalLoginInfoScript(this HtmlHelper html, IEnumerable externalLoginErrors) + public static IHtmlString AngularValueExternalLoginInfoScript(this HtmlHelper html, BackOfficeExternalLoginProviderErrors externalLoginErrors) { - var loginProviders = html.ViewContext.HttpContext.GetOwinContext().Authentication.GetExternalAuthenticationTypes() - .Where(p => p.Properties.ContainsKey("UmbracoBackOffice")) + var loginProviders = html.ViewContext.HttpContext.GetOwinContext().Authentication.GetBackOfficeExternalLoginProviders() .Select(p => new { authType = p.AuthenticationType, @@ -74,13 +76,15 @@ public static IHtmlString AngularValueExternalLoginInfoScript(this HtmlHelper ht if (externalLoginErrors != null) { - foreach (var error in externalLoginErrors) + foreach (var error in externalLoginErrors.Errors) { - sb.AppendFormat(@"errors.push(""{0}"");", error).AppendLine(); + sb.AppendFormat(@"errors.push(""{0}"");", error.ToSingleLine()).AppendLine(); } } sb.AppendLine(@"app.value(""externalLoginInfo"", {"); + if (externalLoginErrors?.AuthenticationType != null) + sb.AppendLine($@"errorProvider: '{externalLoginErrors.AuthenticationType}',"); sb.AppendLine(@"errors: errors,"); sb.Append(@"providers: "); sb.AppendLine(JsonConvert.SerializeObject(loginProviders)); @@ -89,6 +93,12 @@ public static IHtmlString AngularValueExternalLoginInfoScript(this HtmlHelper ht return html.Raw(sb.ToString()); } + [Obsolete("Use the other overload instead")] + public static IHtmlString AngularValueExternalLoginInfoScript(this HtmlHelper html, IEnumerable externalLoginErrors) + { + return html.AngularValueExternalLoginInfoScript(new BackOfficeExternalLoginProviderErrors(string.Empty, externalLoginErrors)); + } + /// /// Used to render the script that will pass in the angular "resetPasswordCodeInfo" service/value on page load /// diff --git a/src/Umbraco.Web/HttpCookieExtensions.cs b/src/Umbraco.Web/HttpCookieExtensions.cs index 5f520653f5ac..ebb77bd4a490 100644 --- a/src/Umbraco.Web/HttpCookieExtensions.cs +++ b/src/Umbraco.Web/HttpCookieExtensions.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Web; using Microsoft.Owin; +using Newtonsoft.Json; using Umbraco.Core; namespace Umbraco.Web @@ -61,11 +63,11 @@ public static void ExpireCookie(this HttpContextBase http, string cookieName) http.Request.Cookies.Remove(cookieName); //expire from the response - var angularCookie = http.Response.Cookies[cookieName]; - if (angularCookie != null) + var cookie = http.Response.Cookies[cookieName]; + if (cookie != null) { //this will expire immediately and be removed from the browser - angularCookie.Expires = DateTime.Now.AddYears(-1); + cookie.Expires = DateTime.Now.AddYears(-1); } else { diff --git a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs index 7ac5578d75c6..f39b267e18b0 100644 --- a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web public static class ImageCropperTemplateCoreExtensions { /// - /// Gets the ImageProcessor Url by the crop alias (from the "umbracoFile" property alias) on the IPublishedContent item + /// Gets the ImageProcessor URL by the crop alias (from the "umbracoFile" property alias) on the IPublishedContent item /// /// /// The IPublishedContent item. @@ -21,7 +21,7 @@ public static class ImageCropperTemplateCoreExtensions /// The crop alias e.g. thumbnail /// /// - /// The ImageProcessor.Web Url. + /// The ImageProcessor.Web URL. /// public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, IImageUrlGenerator imageUrlGenerator) { @@ -29,7 +29,7 @@ public static string GetCropUrl(this IPublishedContent mediaItem, string cropAli } /// - /// Gets the ImageProcessor Url by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. + /// Gets the ImageProcessor URL by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. /// /// /// The IPublishedContent item. @@ -41,7 +41,7 @@ public static string GetCropUrl(this IPublishedContent mediaItem, string cropAli /// The crop alias e.g. thumbnail /// /// - /// The ImageProcessor.Web Url. + /// The ImageProcessor.Web URL. /// public static string GetCropUrl(this IPublishedContent mediaItem, string propertyAlias, string cropAlias, IImageUrlGenerator imageUrlGenerator) { @@ -49,7 +49,7 @@ public static string GetCropUrl(this IPublishedContent mediaItem, string propert } /// - /// Gets the ImageProcessor Url from the IPublishedContent item. + /// Gets the ImageProcessor URL from the IPublishedContent item. /// /// /// The IPublishedContent item. @@ -156,10 +156,10 @@ public static string GetCropUrl( } /// - /// Gets the ImageProcessor Url from the image path. + /// Gets the ImageProcessor URL from the image path. /// /// - /// The image url. + /// The image URL. /// /// /// The width of the output image. @@ -238,13 +238,13 @@ public static string GetCropUrl( } /// - /// Gets the ImageProcessor Url from the image path. + /// Gets the ImageProcessor URL from the image path. /// /// - /// The image url. + /// The image URL. /// /// - /// The generator that will process all the options and the image URL to return a full image urls with all processing options appended + /// The generator that will process all the options and the image URL to return a full image URLs with all processing options appended /// /// /// diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index f3833ba42a71..26dd2a5d36c6 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -13,12 +13,12 @@ namespace Umbraco.Web { /// - /// Provides extension methods for getting ImageProcessor Url from the core Image Cropper property editor + /// Provides extension methods for getting ImageProcessor URL from the core Image Cropper property editor /// public static class ImageCropperTemplateExtensions { /// - /// Gets the ImageProcessor Url by the crop alias (from the "umbracoFile" property alias) on the IPublishedContent item + /// Gets the ImageProcessor URL by the crop alias (from the "umbracoFile" property alias) on the IPublishedContent item /// /// /// The IPublishedContent item. @@ -27,12 +27,12 @@ public static class ImageCropperTemplateExtensions /// The crop alias e.g. thumbnail /// /// - /// The ImageProcessor.Web Url. + /// The ImageProcessor.Web URL. /// public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, cropAlias, Current.ImageUrlGenerator); /// - /// Gets the ImageProcessor Url by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. + /// Gets the ImageProcessor URL by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. /// /// /// The IPublishedContent item. @@ -44,12 +44,12 @@ public static class ImageCropperTemplateExtensions /// The crop alias e.g. thumbnail /// /// - /// The ImageProcessor.Web Url. + /// The ImageProcessor.Web URL. /// public static string GetCropUrl(this IPublishedContent mediaItem, string propertyAlias, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, propertyAlias, cropAlias, Current.ImageUrlGenerator); /// - /// Gets the ImageProcessor Url from the IPublishedContent item. + /// Gets the ImageProcessor URL from the IPublishedContent item. /// /// /// The IPublishedContent item. @@ -118,10 +118,10 @@ public static string GetCropUrl( bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, Current.ImageUrlGenerator, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); /// - /// Gets the ImageProcessor Url from the image path. + /// Gets the ImageProcessor URL from the image path. /// /// - /// The image url. + /// The image URL. /// /// /// The width of the output image. @@ -187,10 +187,10 @@ public static string GetCropUrl( bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(imageUrl, Current.ImageUrlGenerator, width, height, imageCropperValue, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); /// - /// Gets the ImageProcessor Url from the image path. + /// Gets the ImageProcessor URL from the image path. /// /// - /// The image url. + /// The image URL. /// /// /// diff --git a/src/Umbraco.Web/Install/Controllers/InstallController.cs b/src/Umbraco.Web/Install/Controllers/InstallController.cs index 902ebbcd539a..fa40ea517ccc 100644 --- a/src/Umbraco.Web/Install/Controllers/InstallController.cs +++ b/src/Umbraco.Web/Install/Controllers/InstallController.cs @@ -61,7 +61,7 @@ public ActionResult Index() } } - // gen the install base url + // gen the install base URL ViewData.SetInstallApiBaseUrl(Url.GetUmbracoApiService("GetSetup", "InstallApi", "UmbracoInstall").TrimEnd("GetSetup")); // get the base umbraco folder diff --git a/src/Umbraco.Web/Logging/WebProfilerComponent.cs b/src/Umbraco.Web/Logging/WebProfilerComponent.cs index ff25eb6a22b3..e02f082aa9a5 100755 --- a/src/Umbraco.Web/Logging/WebProfilerComponent.cs +++ b/src/Umbraco.Web/Logging/WebProfilerComponent.cs @@ -6,7 +6,7 @@ namespace Umbraco.Web.Logging { - internal sealed class WebProfilerComponent : IComponent + public sealed class WebProfilerComponent : IComponent { private readonly WebProfiler _profiler; private readonly bool _profile; diff --git a/src/Umbraco.Web/Logging/WebProfilerComposer.cs b/src/Umbraco.Web/Logging/WebProfilerComposer.cs index 5834dd9dd4f0..af1d2376cb94 100644 --- a/src/Umbraco.Web/Logging/WebProfilerComposer.cs +++ b/src/Umbraco.Web/Logging/WebProfilerComposer.cs @@ -2,6 +2,6 @@ namespace Umbraco.Web.Logging { - internal class WebProfilerComposer : ComponentComposer, ICoreComposer + public sealed class WebProfilerComposer : ComponentComposer, ICoreComposer { } } diff --git a/src/Umbraco.Web/Media/EmbedProviders/EmbedProviderBase.cs b/src/Umbraco.Web/Media/EmbedProviders/EmbedProviderBase.cs index 9e34815def5e..6dc7c76d8edc 100644 --- a/src/Umbraco.Web/Media/EmbedProviders/EmbedProviderBase.cs +++ b/src/Umbraco.Web/Media/EmbedProviders/EmbedProviderBase.cs @@ -25,7 +25,7 @@ public abstract class EmbedProviderBase : IEmbedProvider public virtual string GetEmbedProviderUrl(string url, int maxWidth, int maxHeight) { if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) == false) - throw new ArgumentException("Not a valid Url"); + throw new ArgumentException("Not a valid URL.", nameof(url)); var fullUrl = new StringBuilder(); diff --git a/src/Umbraco.Web/Models/ContentEditing/CodeFileDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/CodeFileDisplay.cs index 8c89bf263e9f..36ad82444a19 100644 --- a/src/Umbraco.Web/Models/ContentEditing/CodeFileDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/CodeFileDisplay.cs @@ -27,7 +27,7 @@ public CodeFileDisplay() /// /// Path represents the path used by the backoffice tree - /// For files stored on disk, this is a urlencoded, comma separated + /// For files stored on disk, this is a URL encoded, comma separated /// path to the file, always starting with -1. /// /// -1,Partials,Parials%2FFolder,Partials%2FFolder%2FFile.cshtml diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index 50ed4684d954..983172f8e192 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -173,7 +173,7 @@ private UrlInfo[] GetUrls(IContent source) var umbracoContext = _umbracoContextAccessor.UmbracoContext; var urls = umbracoContext == null - ? new[] { UrlInfo.Message("Cannot generate urls without a current Umbraco Context") } + ? new[] { UrlInfo.Message("Cannot generate URLs without a current Umbraco Context") } : source.GetContentUrls(_publishedRouter, umbracoContext, _localizationService, _localizedTextService, _contentService, _logger).ToArray(); return urls; diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index d6a57a66d729..55e8a8687a94 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -9,7 +9,7 @@ namespace Umbraco.Web.Models /// /// Provide an abstract base class for IPublishedContent implementations. /// - /// This base class does which (a) consistently resolves and caches the Url, (b) provides an implementation + /// This base class does which (a) consistently resolves and caches the URL, (b) provides an implementation /// for this[alias], and (c) provides basic content set management. [DebuggerDisplay("Content Id: {Id}")] public abstract class PublishedContentBase : IPublishedContent diff --git a/src/Umbraco.Web/Models/Trees/MenuItem.cs b/src/Umbraco.Web/Models/Trees/MenuItem.cs index 094c6b24ff0b..fb4dfb836c43 100644 --- a/src/Umbraco.Web/Models/Trees/MenuItem.cs +++ b/src/Umbraco.Web/Models/Trees/MenuItem.cs @@ -165,7 +165,7 @@ public void LaunchDialogView(string view, string dialogTitle) } /// - /// Sets the menu item to display a dialog based on a url path in an iframe + /// Sets the menu item to display a dialog based on a URL path in an iframe /// /// /// diff --git a/src/Umbraco.Web/Models/Trees/TreeNode.cs b/src/Umbraco.Web/Models/Trees/TreeNode.cs index dc383bd18bbb..10684b519eb8 100644 --- a/src/Umbraco.Web/Models/Trees/TreeNode.cs +++ b/src/Umbraco.Web/Models/Trees/TreeNode.cs @@ -64,13 +64,13 @@ internal TreeNode(string nodeId, string parentId, string getChildNodesUrl, strin public string RoutePath { get; set; } /// - /// The JSON url to load the nodes children + /// The JSON URL to load the nodes children /// [DataMember(Name = "childNodesUrl")] public string ChildNodesUrl { get; set; } /// - /// The JSON url to load the menu from + /// The JSON URL to load the menu from /// [DataMember(Name = "menuUrl")] public string MenuUrl { get; set; } diff --git a/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs b/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs index 493d9deed2c6..82a9c42235c7 100644 --- a/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs +++ b/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs @@ -66,11 +66,11 @@ internal static Route RouteControllerPlugin(this AreaRegistration area, //var meta = PluginController.GetMetadata(controllerType); if (isMvc) { - //create a new route with custom name, specified url, and the namespace of the controller plugin + //create a new route with custom name, specified URL, and the namespace of the controller plugin controllerPluginRoute = routes.MapRoute( //name string.Format("umbraco-{0}-{1}", area.AreaName, controllerName), - //url format + //URL format url, //set the namespace of the controller to match new[] {controllerType.Namespace}); @@ -89,7 +89,7 @@ internal static Route RouteControllerPlugin(this AreaRegistration area, controllerPluginRoute = routes.MapHttpRoute( //name string.Format("umbraco-{0}-{1}-{2}", "api", area.AreaName, controllerName), - //url format + //URL format url, new {controller = controllerName, id = defaultId}); //web api routes don't set the data tokens object diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index 64bdac669f83..28f2449187ab 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -155,10 +155,10 @@ internal static PostedDataProxyInfo GetFormInfo(RequestContext requestContext) }; } - + /// - /// Handles a posted form to an Umbraco Url and ensures the correct controller is routed to and that + /// Handles a posted form to an Umbraco URL and ensures the correct controller is routed to and that /// the right DataTokens are set. /// /// diff --git a/src/Umbraco.Web/OwinExtensions.cs b/src/Umbraco.Web/OwinExtensions.cs index 6961f5c91589..e7d41d113cff 100644 --- a/src/Umbraco.Web/OwinExtensions.cs +++ b/src/Umbraco.Web/OwinExtensions.cs @@ -5,13 +5,28 @@ using Microsoft.Owin.Security; using Umbraco.Core; using Umbraco.Core.Models.Identity; -using Umbraco.Core.Security; using Umbraco.Web.Security; namespace Umbraco.Web { public static class OwinExtensions { + /// + /// Used by external login providers to set any errors that occur during the OAuth negotiation + /// + /// + /// + public static void SetExternalLoginProviderErrors(this IOwinContext owinContext, BackOfficeExternalLoginProviderErrors errors) + => owinContext.Set(errors); + + /// + /// Retrieve any errors set by external login providers during OAuth negotiation + /// + /// + /// + internal static BackOfficeExternalLoginProviderErrors GetExternalLoginProviderErrors(this IOwinContext owinContext) + => owinContext.Get(); + /// /// Gets the for the Umbraco back office cookie /// diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs index 8af2d98018bd..522a81d7b2d8 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.PropertyEditors [DataEditor( Constants.PropertyEditors.Aliases.MultiUrlPicker, EditorType.PropertyValue, - "Multi Url Picker", + "Multi URL Picker", "multiurlpicker", ValueType = ValueTypes.Json, Group = Constants.PropertyEditors.Groups.Pickers, diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index 853e995ed82a..5a84e4b20cd0 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -141,7 +141,7 @@ from link in JsonConvert.DeserializeObject>(value) QueryString = link.QueryString, Target = link.Target, Udi = link.Udi, - Url = link.Udi == null ? link.Url : null, // only save the url for external links + Url = link.Udi == null ? link.Url : null, // only save the URL for external links }, new JsonSerializerSettings { diff --git a/src/Umbraco.Web/PropertyEditors/PropertyEditorsComponent.cs b/src/Umbraco.Web/PropertyEditors/PropertyEditorsComponent.cs index eaa209c05571..cd7b7a1f3999 100644 --- a/src/Umbraco.Web/PropertyEditors/PropertyEditorsComponent.cs +++ b/src/Umbraco.Web/PropertyEditors/PropertyEditorsComponent.cs @@ -10,7 +10,7 @@ namespace Umbraco.Web.PropertyEditors { - internal sealed class PropertyEditorsComponent : IComponent + public sealed class PropertyEditorsComponent : IComponent { private readonly PropertyEditorCollection _propertyEditors; private readonly List _terminate = new List(); diff --git a/src/Umbraco.Web/PropertyEditors/PropertyEditorsComposer.cs b/src/Umbraco.Web/PropertyEditors/PropertyEditorsComposer.cs index ce0cf2904f7d..f22d18484b10 100644 --- a/src/Umbraco.Web/PropertyEditors/PropertyEditorsComposer.cs +++ b/src/Umbraco.Web/PropertyEditors/PropertyEditorsComposer.cs @@ -4,6 +4,6 @@ namespace Umbraco.Web.PropertyEditors { [RuntimeLevel(MinLevel = RuntimeLevel.Run)] - internal class PropertyEditorsComposer : ComponentComposer, ICoreComposer + public sealed class PropertyEditorsComposer : ComponentComposer, ICoreComposer { } } diff --git a/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs index 67ff82a3e12d..5a3334685c33 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs @@ -96,7 +96,7 @@ internal string FindAndPersistPastedTempImages(string html, Guid mediaParentFold // Add the UDI to the img element as new data attribute img.SetAttributeValue("data-udi", udi.ToString()); - // Get the new persisted image url + // Get the new persisted image URL var mediaTyped = _umbracoContextAccessor?.UmbracoContext?.Media.GetById(udi.Guid); if (mediaTyped == null) throw new PanicException($"Could not find media by id {udi.Guid} or there was no UmbracoContext available."); diff --git a/src/Umbraco.Web/PropertyEditors/RteEmbedController.cs b/src/Umbraco.Web/PropertyEditors/RteEmbedController.cs index b3498c16ae2e..ca3bd3637414 100644 --- a/src/Umbraco.Web/PropertyEditors/RteEmbedController.cs +++ b/src/Umbraco.Web/PropertyEditors/RteEmbedController.cs @@ -29,8 +29,8 @@ public OEmbedResult GetEmbed(string url, int width, int height) foreach (var provider in _embedCollection) { - //Url Scheme Regex is an array of possible regex patterns to match against the URL - foreach(var urlPattern in provider.UrlSchemeRegex) + // UrlSchemeRegex is an array of possible regex patterns to match against the URL + foreach (var urlPattern in provider.UrlSchemeRegex) { var regexPattern = new Regex(urlPattern, RegexOptions.IgnoreCase); if (regexPattern.IsMatch(url)) @@ -60,7 +60,7 @@ public OEmbedResult GetEmbed(string url, int width, int height) } catch(Exception ex) { - Logger.Error(ex, "Error embedding url {Url} - width: {Width} height: {Height}", url, width, height); + Logger.Error(ex, "Error embedding URL {Url} - width: {Width} height: {Height}", url, width, height); result.OEmbedStatus = OEmbedStatus.Error; } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs index 8b3655f0ccb3..5e7cf8fea4e9 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs @@ -36,9 +36,14 @@ public override object ConvertSourceToIntermediate(IPublishedElement owner, IPub { if (source == null) return null; - var attemptConvertInt = source.TryConvertTo(); - if (attemptConvertInt.Success) - return attemptConvertInt.Result; + //Don't attempt to convert to int for UDI + if(!(source is string) || source is string strSource && !string.IsNullOrWhiteSpace(strSource) && !strSource.StartsWith("umb")) + { + var attemptConvertInt = source.TryConvertTo(); + if (attemptConvertInt.Success) + return attemptConvertInt.Result; + } + var attemptConvertUdi = source.TryConvertTo(); if (attemptConvertUdi.Success) return attemptConvertUdi.Result; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index 5c027ed9b64d..8abb77839d4a 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -41,7 +41,7 @@ public override object ConvertSourceToIntermediate(IPublishedElement owner, IPub if (source == null) return null; var sourceString = source.ToString(); - // ensures string is parsed for {localLink} and urls are resolved correctly + // ensures string is parsed for {localLink} and URLs are resolved correctly sourceString = _localLinkParser.EnsureInternalLinks(sourceString, preview); sourceString = _urlParser.EnsureUrls(sourceString); diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs index 3ab502742cc5..46fda1ea590d 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -87,7 +87,7 @@ private string Convert(object source, bool preview) var sourceString = source.ToString(); - // ensures string is parsed for {localLink} and urls and media are resolved correctly + // ensures string is parsed for {localLink} and URLs and media are resolved correctly sourceString = _linkParser.EnsureInternalLinks(sourceString, preview); sourceString = _urlParser.EnsureUrls(sourceString); sourceString = _imageSourceParser.EnsureImageSources(sourceString); diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs index 939a658407d8..65c0deeacfb6 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs @@ -39,7 +39,7 @@ public override object ConvertSourceToIntermediate(IPublishedElement owner, IPub if (source == null) return null; var sourceString = source.ToString(); - // ensures string is parsed for {localLink} and urls are resolved correctly + // ensures string is parsed for {localLink} and URLs are resolved correctly sourceString = _linkParser.EnsureInternalLinks(sourceString, preview); sourceString = _urlParser.EnsureUrls(sourceString); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs index 8e6e517aea6f..a0224f90ead9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs @@ -109,8 +109,8 @@ private IPublishedContent GetByRouteInternal(bool preview, string route, bool? h content = FollowRoute(content, parts, 1, culture); } - // if hideTopLevelNodePath is true then for url /foo we looked for /*/foo - // but maybe that was the url of a non-default top-level node, so we also + // if hideTopLevelNodePath is true then for URL /foo we looked for /*/foo + // but maybe that was the URL of a non-default top-level node, so we also // have to look for /foo (see note in ApplyHideTopLevelNodeFromPath). if (content == null && hideTopLevelNode.Value && parts.Length == 1) { @@ -141,7 +141,7 @@ private string GetRouteByIdInternal(bool preview, int contentId, bool? hideTopLe hideTopLevelNode = hideTopLevelNode ?? HideTopLevelNodeFromPath; // default = settings // walk up from that node until we hit a node with a domain, - // or we reach the content root, collecting urls in the way + // or we reach the content root, collecting URLs in the way var pathParts = new List(); var n = node; var urlSegment = n.UrlSegment(culture); diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 73148b667a2e..f4c11977dcea 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1333,25 +1333,25 @@ private static Dictionary GetAliasesAndNames(IContentTypeBase co #region Url /// - /// Gets the url of the content item. + /// Gets the URL of the content item. /// /// - /// If the content item is a document, then this method returns the url of the - /// document. If it is a media, then this methods return the media url for the - /// 'umbracoFile' property. Use the MediaUrl() method to get the media url for other + /// If the content item is a document, then this method returns the URL of the + /// document. If it is a media, then this methods return the media URL for the + /// 'umbracoFile' property. Use the MediaUrl() method to get the media URL for other /// properties. /// The value of this property is contextual. It depends on the 'current' request uri, - /// if any. In addition, when the content type is multi-lingual, this is the url for the - /// specified culture. Otherwise, it is the invariant url. + /// if any. In addition, when the content type is multi-lingual, this is the URL for the + /// specified culture. Otherwise, it is the invariant URL. /// public static string Url(this IPublishedContent content, string culture = null, UrlMode mode = UrlMode.Default) { var umbracoContext = Composing.Current.UmbracoContext; if (umbracoContext == null) - throw new InvalidOperationException("Cannot resolve a Url when Current.UmbracoContext is null."); + throw new InvalidOperationException("Cannot resolve a URL when Current.UmbracoContext is null."); if (umbracoContext.UrlProvider == null) - throw new InvalidOperationException("Cannot resolve a Url when Current.UmbracoContext.UrlProvider is null."); + throw new InvalidOperationException("Cannot resolve a URL when Current.UmbracoContext.UrlProvider is null."); switch (content.ContentType.ItemType) { diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index f28581eb4229..4c71e75f85c5 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -193,17 +193,17 @@ public static bool IsVisible(this IPublishedElement content) #region MediaUrl /// - /// Gets the url for a media. + /// Gets the URL for a media. /// /// The content item. /// The culture (use current culture by default). - /// The url mode (use site configuration by default). + /// The URL mode (use site configuration by default). /// The alias of the property (use 'umbracoFile' by default). - /// The url for the media. + /// The URL for the media. /// /// The value of this property is contextual. It depends on the 'current' request uri, - /// if any. In addition, when the content type is multi-lingual, this is the url for the - /// specified culture. Otherwise, it is the invariant url. + /// if any. In addition, when the content type is multi-lingual, this is the URL for the + /// specified culture. Otherwise, it is the invariant URL. /// public static string MediaUrl(this IPublishedContent content, string culture = null, UrlMode mode = UrlMode.Default, string propertyAlias = Constants.Conventions.Media.File) { diff --git a/src/Umbraco.Web/RoutableDocumentFilter.cs b/src/Umbraco.Web/RoutableDocumentFilter.cs index 9725ce0e14ef..507a40e471fa 100644 --- a/src/Umbraco.Web/RoutableDocumentFilter.cs +++ b/src/Umbraco.Web/RoutableDocumentFilter.cs @@ -45,7 +45,7 @@ public bool IsDocumentRequest(HttpContextBase httpContext, Uri uri) var maybeDoc = true; var lpath = uri.AbsolutePath.ToLowerInvariant(); - // handle directory-urls used for asmx + // handle directory-URLs used for asmx // TODO: legacy - what's the point really? var asmxPos = lpath.IndexOf(".asmx/", StringComparison.OrdinalIgnoreCase); if (asmxPos >= 0) @@ -124,7 +124,7 @@ internal bool IsReservedPathOrUrl(string url) return newReservedList; }); - //The url should be cleaned up before checking: + //The URL should be cleaned up before checking: // * If it doesn't contain an '.' in the path then we assume it is a path based URL, if that is the case we should add an trailing '/' because all of our reservedPaths use a trailing '/' // * We shouldn't be comparing the query at all var pathPart = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0].ToLowerInvariant(); @@ -133,7 +133,7 @@ internal bool IsReservedPathOrUrl(string url) pathPart = pathPart.EnsureEndsWith('/'); } - // return true if url starts with an element of the reserved list + // return true if URL starts with an element of the reserved list return _reservedList.Any(x => pathPart.InvariantStartsWith(x)); } diff --git a/src/Umbraco.Web/Routing/AliasUrlProvider.cs b/src/Umbraco.Web/Routing/AliasUrlProvider.cs index f84ac608d26d..b3b56a18e719 100644 --- a/src/Umbraco.Web/Routing/AliasUrlProvider.cs +++ b/src/Umbraco.Web/Routing/AliasUrlProvider.cs @@ -9,7 +9,7 @@ namespace Umbraco.Web.Routing { /// - /// Provides urls using the umbracoUrlAlias property. + /// Provides URLs using the umbracoUrlAlias property. /// public class AliasUrlProvider : IUrlProvider { @@ -41,15 +41,15 @@ public UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, #region GetOtherUrls /// - /// Gets the other urls of a published content. + /// Gets the other URLs of a published content. /// /// The Umbraco context. /// The published content id. - /// The current absolute url. - /// The other urls for the published content. + /// The current absolute URL. + /// The other URLs for the published content. /// - /// Other urls are those that GetUrl would not return in the current context, but would be valid - /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// Other URLs are those that GetUrl would not return in the current context, but would be valid + /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). /// public IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current) { @@ -76,7 +76,7 @@ public IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, if (domainUris == null) { // no domain - // if the property is invariant, then url "/" is ok + // if the property is invariant, then URL "/" is ok // if the property varies, then what are we supposed to do? // the content finder may work, depending on the 'current' culture, // but there's no way we can return something meaningful here @@ -98,11 +98,11 @@ public IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, } else { - // some domains: one url per domain, which is "/" - foreach(var domainUri in domainUris) + // some domains: one URL per domain, which is "/" + foreach (var domainUri in domainUris) { - // if the property is invariant, get the invariant value, url is "/" - // if the property varies, get the variant value, url is "/" + // if the property is invariant, get the invariant value, URL is "/" + // if the property varies, get the variant value, URL is "/" // but! only if the culture is published, else ignore if (varies && !node.HasCulture(domainUri.Culture.Name)) continue; diff --git a/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs b/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs index 46e44dc5a30e..2781eed37d36 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs @@ -7,7 +7,7 @@ namespace Umbraco.Web.Routing { /// - /// Provides an implementation of that handles page url rewrites + /// Provides an implementation of that handles page URL rewrites /// that are stored when moving, saving, or deleting a node. /// /// @@ -48,14 +48,14 @@ public bool TryFindContent(PublishedRequest frequest) var url = content == null ? "#" : content.Url(redirectUrl.Culture); if (url.StartsWith("#")) { - _logger.Debug("Route {Route} matches content {ContentId} which has no url.", route, redirectUrl.ContentId); + _logger.Debug("Route {Route} matches content {ContentId} which has no URL.", route, redirectUrl.ContentId); return false; } - // Appending any querystring from the incoming request to the redirect url. + // Appending any querystring from the incoming request to the redirect URL url = string.IsNullOrEmpty(frequest.Uri.Query) ? url : url + frequest.Uri.Query; - _logger.Debug("Route {Route} matches content {ContentId} with url '{Url}', redirecting.", route, content.Id, url); + _logger.Debug("Route {Route} matches content {ContentId} with URL '{Url}', redirecting.", route, content.Id, url); frequest.SetRedirectPermanent(url); diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrl.cs b/src/Umbraco.Web/Routing/ContentFinderByUrl.cs index 0a14dc97feb3..074193417ab8 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrl.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrl.cs @@ -5,10 +5,10 @@ namespace Umbraco.Web.Routing { /// - /// Provides an implementation of that handles page nice urls. + /// Provides an implementation of that handles page nice URLs. /// /// - /// Handles /foo/bar where /foo/bar is the nice url of a document. + /// Handles /foo/bar where /foo/bar is the nice URL of a document. /// public class ContentFinderByUrl : IContentFinder { diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs b/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs index cf7161104730..9340c6c6de0e 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs @@ -96,7 +96,7 @@ bool IsMatch(IPublishedContent c, string a1, string a2) } // TODO: even with Linq, what happens below has to be horribly slow - // but the only solution is to entirely refactor url providers to stop being dynamic + // but the only solution is to entirely refactor URL providers to stop being dynamic if (rootNodeId > 0) { diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs b/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs index 16dfa63596cd..a02ebbb2a8bb 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs @@ -9,11 +9,11 @@ namespace Umbraco.Web.Routing { /// - /// Provides an implementation of that handles page nice urls and a template. + /// Provides an implementation of that handles page nice URLs and a template. /// /// /// This finder allows for an odd routing pattern similar to altTemplate, probably only use case is if there is an alternative mime type template and it should be routable by something like "/hello/world/json" where the JSON template is to be used for the "world" page - /// Handles /foo/bar/template where /foo/bar is the nice url of a document, and template a template alias. + /// Handles /foo/bar/template where /foo/bar is the nice URL of a document, and template a template alias. /// If successful, then the template of the document request is also assigned. /// public class ContentFinderByUrlAndTemplate : ContentFinderByUrl diff --git a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs index 89abde057663..48d173816d5a 100644 --- a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs @@ -6,7 +6,7 @@ namespace Umbraco.Web.Routing { /// - /// Default media url provider. + /// Default media URL provider. /// public class DefaultMediaUrlProvider : IMediaUrlProvider { diff --git a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs index 4092538481d7..291ac67882bd 100644 --- a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs @@ -31,7 +31,7 @@ public DefaultUrlProvider(IRequestHandlerSection requestSettings, ILogger logger /// public virtual UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlMode mode, string culture, Uri current) { - if (!current.IsAbsoluteUri) throw new ArgumentException("Current url must be absolute.", nameof(current)); + if (!current.IsAbsoluteUri) throw new ArgumentException("Current URL must be absolute.", nameof(current)); // will not use cache if previewing var route = umbracoContext.Content.GetRouteById(content.Id, culture); @@ -55,7 +55,7 @@ internal UrlInfo GetUrlFromRoute(string route, UmbracoContext umbracoContext, in ? null : DomainUtilities.DomainForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainHelper, int.Parse(route.Substring(0, pos)), current, culture); - // assemble the url from domainUri (maybe null) and path + // assemble the URL from domainUri (maybe null) and path var url = AssembleUrl(domainUri, path, current, mode).ToString(); return UrlInfo.Url(url, culture); @@ -66,15 +66,15 @@ internal UrlInfo GetUrlFromRoute(string route, UmbracoContext umbracoContext, in #region GetOtherUrls /// - /// Gets the other urls of a published content. + /// Gets the other URLs of a published content. /// /// The Umbraco context. /// The published content id. - /// The current absolute url. - /// The other urls for the published content. + /// The current absolute URL. + /// The other URLs for the published content. /// - /// Other urls are those that GetUrl would not return in the current context, but would be valid - /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// Other URLs are those that GetUrl would not return in the current context, but would be valid + /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). /// public virtual IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current) { @@ -166,7 +166,7 @@ Uri AssembleUrl(DomainAndUri domainUri, string path, Uri current, UrlMode mode) } // UriFromUmbraco will handle vdir - // meaning it will add vdir into domain urls too! + // meaning it will add vdir into domain URLs too! return UriUtility.UriFromUmbraco(uri, _globalSettings, _requestSettings); } diff --git a/src/Umbraco.Web/Routing/IMediaUrlProvider.cs b/src/Umbraco.Web/Routing/IMediaUrlProvider.cs index 8a81b27415a8..efb005907d11 100644 --- a/src/Umbraco.Web/Routing/IMediaUrlProvider.cs +++ b/src/Umbraco.Web/Routing/IMediaUrlProvider.cs @@ -4,27 +4,27 @@ namespace Umbraco.Web.Routing { /// - /// Provides media urls. + /// Provides media URL. /// public interface IMediaUrlProvider { /// - /// Gets the url of a media item. + /// Gets the URL of a media item. /// /// The Umbraco context. /// The published content. - /// The property alias to resolve the url from. - /// The url mode. + /// The property alias to resolve the URL from. + /// The URL mode. /// The variation language. - /// The current absolute url. - /// The url for the media. + /// The current absolute URL. + /// The URL for the media. /// - /// The url is absolute or relative depending on mode and on current. - /// If the media is multi-lingual, gets the url for the specified culture or, + /// The URL is absolute or relative depending on mode and on current. + /// If the media is multi-lingual, gets the URL for the specified culture or, /// when no culture is specified, the current culture. - /// The url provider can ignore the mode and always return an absolute url, - /// e.g. a cdn url provider will most likely always return an absolute url. - /// If the provider is unable to provide a url, it returns null. + /// The URL provider can ignore the mode and always return an absolute URL, + /// e.g. a cdn URL provider will most likely always return an absolute URL. + /// If the provider is unable to provide a URL, it returns null. /// UrlInfo GetMediaUrl(UmbracoContext umbracoContext, IPublishedContent content, string propertyAlias, UrlMode mode, string culture, Uri current); } diff --git a/src/Umbraco.Web/Routing/IUrlProvider.cs b/src/Umbraco.Web/Routing/IUrlProvider.cs index c0ce1fef3986..e7d5628fd08e 100644 --- a/src/Umbraco.Web/Routing/IUrlProvider.cs +++ b/src/Umbraco.Web/Routing/IUrlProvider.cs @@ -5,37 +5,37 @@ namespace Umbraco.Web.Routing { /// - /// Provides urls. + /// Provides URLs. /// public interface IUrlProvider { /// - /// Gets the url of a published content. + /// Gets the URL of a published content. /// /// The Umbraco context. /// The published content. - /// The url mode. + /// The URL mode. /// A culture. - /// The current absolute url. - /// The url for the published content. + /// The current absolute URL. + /// The URL for the published content. /// - /// The url is absolute or relative depending on mode and on current. - /// If the published content is multi-lingual, gets the url for the specified culture or, + /// The URL is absolute or relative depending on mode and on current. + /// If the published content is multi-lingual, gets the URL for the specified culture or, /// when no culture is specified, the current culture. - /// If the provider is unable to provide a url, it should return null. + /// If the provider is unable to provide a URL, it should return null. /// UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlMode mode, string culture, Uri current); /// - /// Gets the other urls of a published content. + /// Gets the other URLs of a published content. /// /// The Umbraco context. /// The published content id. - /// The current absolute url. - /// The other urls for the published content. + /// The current absolute URL. + /// The other URLs for the published content. /// - /// Other urls are those that GetUrl would not return in the current context, but would be valid - /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// Other URLs are those that GetUrl would not return in the current context, but would be valid + /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). /// IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current); } diff --git a/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs b/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs index e1f9022f27b9..0045cf33dc2d 100644 --- a/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs +++ b/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs @@ -30,11 +30,11 @@ internal void WriteOutput(HttpContext context) response.Clear(); var frequest = Current.UmbracoContext.PublishedRequest; - var reason = "Cannot render the page at url '{0}'."; + var reason = "Cannot render the page at URL '{0}'."; if (frequest.HasPublishedContent == false) - reason = "No umbraco document matches the url '{0}'."; + reason = "No umbraco document matches the URL '{0}'."; else if (frequest.HasTemplate == false) - reason = "No template exists to render the document at url '{0}'."; + reason = "No template exists to render the document at URL '{0}'."; response.Write("

Page not found

"); response.Write("

"); diff --git a/src/Umbraco.Web/Routing/PublishedRequest.cs b/src/Umbraco.Web/Routing/PublishedRequest.cs index aee7ba4de815..959f96acc003 100644 --- a/src/Umbraco.Web/Routing/PublishedRequest.cs +++ b/src/Umbraco.Web/Routing/PublishedRequest.cs @@ -369,14 +369,14 @@ public bool Is404 public bool IsRedirectPermanent { get; private set; } /// - /// Gets or sets the url to redirect to, when the content request triggers a redirect. + /// Gets or sets the URL to redirect to, when the content request triggers a redirect. /// public string RedirectUrl { get; private set; } /// /// Indicates that the content request should trigger a redirect (302). /// - /// The url to redirect to. + /// The URL to redirect to. /// Does not actually perform a redirect, only registers that the response should /// redirect. Redirect will or will not take place in due time. public void SetRedirect(string url) @@ -389,7 +389,7 @@ public void SetRedirect(string url) /// /// Indicates that the content request should trigger a permanent redirect (301). /// - /// The url to redirect to. + /// The URL to redirect to. /// Does not actually perform a redirect, only registers that the response should /// redirect. Redirect will or will not take place in due time. public void SetRedirectPermanent(string url) @@ -402,7 +402,7 @@ public void SetRedirectPermanent(string url) /// /// Indicates that the content request should trigger a redirect, with a specified status code. /// - /// The url to redirect to. + /// The URL to redirect to. /// The status code (300-308). /// Does not actually perform a redirect, only registers that the response should /// redirect. Redirect will or will not take place in due time. diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index 6e768c28b60d..fee9a621583d 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -96,7 +96,7 @@ public bool PrepareRequest(PublishedRequest request) { // note - at that point the original legacy module did something do handle IIS custom 404 errors // ie pages looking like /anything.aspx?404;/path/to/document - I guess the reason was to support - // "directory urls" without having to do wildcard mapping to ASP.NET on old IIS. This is a pain + // "directory URLs" without having to do wildcard mapping to ASP.NET on old IIS. This is a pain // to maintain and probably not used anymore - removed as of 06/2012. @zpqrtbnk. // // to trigger Umbraco's not-found, one should configure IIS and/or ASP.NET custom 404 errors diff --git a/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs b/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs index 490fade3c653..c12d735a605f 100644 --- a/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs +++ b/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs @@ -11,8 +11,8 @@ namespace Umbraco.Web.Routing { - /// Implements an Application Event Handler for managing redirect urls tracking. - /// when content is renamed or moved, we want to create a permanent 301 redirect from it's old url + /// Implements an Application Event Handler for managing redirect URLs tracking. + /// when content is renamed or moved, we want to create a permanent 301 redirect from it's old URL /// /// not managing domains because we don't know how to do it - changing domains => must create a higher level /// strategy using rewriting rules probably diff --git a/src/Umbraco.Web/Routing/RedirectTrackingComposer.cs b/src/Umbraco.Web/Routing/RedirectTrackingComposer.cs index bcafc8d0508a..a58c7c56790c 100644 --- a/src/Umbraco.Web/Routing/RedirectTrackingComposer.cs +++ b/src/Umbraco.Web/Routing/RedirectTrackingComposer.cs @@ -4,10 +4,10 @@ namespace Umbraco.Web.Routing { /// - /// Implements an Application Event Handler for managing redirect urls tracking. + /// Implements an Application Event Handler for managing redirect URLs tracking. /// /// - /// when content is renamed or moved, we want to create a permanent 301 redirect from it's old url + /// when content is renamed or moved, we want to create a permanent 301 redirect from it's old URL /// not managing domains because we don't know how to do it - changing domains => must create a higher level strategy using rewriting rules probably /// recycle bin = moving to and from does nothing: to = the node is gone, where would we redirect? from = same /// diff --git a/src/Umbraco.Web/Routing/UrlInfo.cs b/src/Umbraco.Web/Routing/UrlInfo.cs index 8385ab54a9cf..59145d19ecfc 100644 --- a/src/Umbraco.Web/Routing/UrlInfo.cs +++ b/src/Umbraco.Web/Routing/UrlInfo.cs @@ -4,14 +4,14 @@ namespace Umbraco.Web.Routing { /// - /// Represents infos for a url. + /// Represents infos for a URL. /// [DataContract(Name = "urlInfo", Namespace = "")] public class UrlInfo : IEquatable { /// - /// Creates a instance representing a true url. + /// Creates a instance representing a true URL. /// public static UrlInfo Url(string text, string culture = null) => new UrlInfo(text, true, culture); @@ -38,14 +38,14 @@ public UrlInfo(string text, bool isUrl, string culture) public string Culture { get; } /// - /// Gets a value indicating whether the url is a true url. + /// Gets a value indicating whether the URL is a true URL. /// /// Otherwise, it is a message. [DataMember(Name = "isUrl")] public bool IsUrl { get; } /// - /// Gets the text, which is either the url, or a message. + /// Gets the text, which is either the URL, or a message. /// [DataMember(Name = "text")] public string Text { get; } diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs index d42639b781c5..5ab4e06c5ef7 100644 --- a/src/Umbraco.Web/Routing/UrlProvider.cs +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -9,19 +9,19 @@ namespace Umbraco.Web.Routing { /// - /// Provides urls. + /// Provides URLs. /// public class UrlProvider { #region Ctor and configuration /// - /// Initializes a new instance of the class with an Umbraco context and a list of url providers. + /// Initializes a new instance of the class with an Umbraco context and a list of URL providers. /// /// The Umbraco context. /// Routing settings. - /// The list of url providers. - /// The list of media url providers. + /// The list of URL providers. + /// The list of media URL providers. /// The current variation accessor. public UrlProvider(UmbracoContext umbracoContext, IWebRoutingSection routingSettings, IEnumerable urlProviders, IEnumerable mediaUrlProviders, IVariationContextAccessor variationContextAccessor) { @@ -41,11 +41,11 @@ public UrlProvider(UmbracoContext umbracoContext, IWebRoutingSection routingSett } /// - /// Initializes a new instance of the class with an Umbraco context and a list of url providers. + /// Initializes a new instance of the class with an Umbraco context and a list of URL providers. /// /// The Umbraco context. - /// The list of url providers. - /// The list of media url providers + /// The list of URL providers. + /// The list of media URL providers /// The current variation accessor. /// An optional provider mode. public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlProviders, IEnumerable mediaUrlProviders, IVariationContextAccessor variationContextAccessor, UrlMode mode = UrlMode.Auto) @@ -64,7 +64,7 @@ public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlP private readonly IVariationContextAccessor _variationContextAccessor; /// - /// Gets or sets the provider url mode. + /// Gets or sets the provider URL mode. /// public UrlMode Mode { get; set; } @@ -78,40 +78,40 @@ public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlP private IPublishedContent GetMedia(Guid id) => _umbracoContext.Media.GetById(id); /// - /// Gets the url of a published content. + /// Gets the URL of a published content. /// /// The published content identifier. - /// The url mode. + /// The URL mode. /// A culture. - /// The current absolute url. - /// The url for the published content. + /// The current absolute URL. + /// The URL for the published content. public string GetUrl(Guid id, UrlMode mode = UrlMode.Default, string culture = null, Uri current = null) => GetUrl(GetDocument(id), mode, culture, current); /// - /// Gets the url of a published content. + /// Gets the URL of a published content. /// /// The published content identifier. - /// The url mode. + /// The URL mode. /// A culture. - /// The current absolute url. - /// The url for the published content. + /// The current absolute URL. + /// The URL for the published content. public string GetUrl(int id, UrlMode mode = UrlMode.Default, string culture = null, Uri current = null) => GetUrl(GetDocument(id), mode, culture, current); /// - /// Gets the url of a published content. + /// Gets the URL of a published content. /// /// The published content. - /// The url mode. + /// The URL mode. /// A culture. - /// The current absolute url. - /// The url for the published content. + /// The current absolute URL. + /// The URL for the published content. /// - /// The url is absolute or relative depending on mode and on current. - /// If the published content is multi-lingual, gets the url for the specified culture or, + /// The URL is absolute or relative depending on mode and on current. + /// If the published content is multi-lingual, gets the URL for the specified culture or, /// when no culture is specified, the current culture. - /// If the provider is unable to provide a url, it returns "#". + /// If the provider is unable to provide a URL, it returns "#". /// public string GetUrl(IPublishedContent content, UrlMode mode = UrlMode.Default, string culture = null, Uri current = null) { @@ -152,14 +152,14 @@ internal string GetUrlFromRoute(int id, string route, string culture) #region GetOtherUrls /// - /// Gets the other urls of a published content. + /// Gets the other URLs of a published content. /// /// The published content id. - /// The other urls for the published content. + /// The other URLs for the published content. /// - /// Other urls are those that GetUrl would not return in the current context, but would be valid - /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). - /// The results depend on the current url. + /// Other URLs are those that GetUrl would not return in the current context, but would be valid + /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// The results depend on the current URL. /// public IEnumerable GetOtherUrls(int id) { @@ -167,14 +167,14 @@ public IEnumerable GetOtherUrls(int id) } /// - /// Gets the other urls of a published content. + /// Gets the other URLs of a published content. /// /// The published content id. - /// The current absolute url. - /// The other urls for the published content. + /// The current absolute URL. + /// The other URLs for the published content. /// - /// Other urls are those that GetUrl would not return in the current context, but would be valid - /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// Other URLs are those that GetUrl would not return in the current context, but would be valid + /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). /// public IEnumerable GetOtherUrls(int id, Uri current) { @@ -186,7 +186,7 @@ public IEnumerable GetOtherUrls(int id, Uri current) #region GetMediaUrl /// - /// Gets the url of a media item. + /// Gets the URL of a media item. /// /// /// @@ -198,19 +198,19 @@ public string GetMediaUrl(Guid id, UrlMode mode = UrlMode.Default, string cultur => GetMediaUrl(GetMedia(id), mode, culture, propertyAlias, current); /// - /// Gets the url of a media item. + /// Gets the URL of a media item. /// /// The published content. - /// The property alias to resolve the url from. - /// The url mode. + /// The property alias to resolve the URL from. + /// The URL mode. /// The variation language. - /// The current absolute url. - /// The url for the media. + /// The current absolute URL. + /// The URL for the media. /// - /// The url is absolute or relative depending on mode and on current. - /// If the media is multi-lingual, gets the url for the specified culture or, + /// The URL is absolute or relative depending on mode and on current. + /// If the media is multi-lingual, gets the URL for the specified culture or, /// when no culture is specified, the current culture. - /// If the provider is unable to provide a url, it returns . + /// If the provider is unable to provide a URL, it returns . /// public string GetMediaUrl(IPublishedContent content, UrlMode mode = UrlMode.Default, string culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri current = null) { diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index 077680d2e25d..afa0bc96ccd7 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -11,11 +11,11 @@ namespace Umbraco.Web.Routing internal static class UrlProviderExtensions { /// - /// Gets the Urls of the content item. + /// Gets the URLs of the content item. /// /// - /// Use when displaying Urls. If errors occur when generating the Urls, they will show in the list. - /// Contains all the Urls that we can figure out (based upon domains, etc). + /// Use when displaying URLs. If errors occur when generating the URLs, they will show in the list. + /// Contains all the URLs that we can figure out (based upon domains, etc). /// public static IEnumerable GetContentUrls(this IContent content, IPublishedRouter publishedRouter, @@ -39,13 +39,13 @@ public static IEnumerable GetContentUrls(this IContent content, yield break; } - // build a list of urls, for the back-office + // build a list of URLs, for the back-office // which will contain - // - the 'main' urls, which is what .Url would return, for each culture - // - the 'other' urls we know (based upon domains, etc) + // - the 'main' URLs, which is what .Url would return, for each culture + // - the 'other' URLs we know (based upon domains, etc) // // need to work through each installed culture: - // on invariant nodes, each culture returns the same url segment but, + // on invariant nodes, each culture returns the same URL segment but, // we don't know if the branch to this content is invariant, so we need to ask // for URLs for all cultures. // and, not only for those assigned to domains in the branch, because we want @@ -61,7 +61,7 @@ public static IEnumerable GetContentUrls(this IContent content, urls.Add(cultureUrl); } - //return the real urls first, then the messages + //return the real URLs first, then the messages foreach (var urlGroup in urls.GroupBy(x => x.IsUrl).OrderByDescending(x => x.Key)) { //in some cases there will be the same URL for multiple cultures: @@ -72,8 +72,8 @@ public static IEnumerable GetContentUrls(this IContent content, yield return dUrl; } - // get the 'other' urls - ie not what you'd get with GetUrl() but urls that would route to the document, nevertheless. - // for these 'other' urls, we don't check whether they are routable, collide, anything - we just report them. + // get the 'other' URLs - ie not what you'd get with GetUrl() but URLs that would route to the document, nevertheless. + // for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them. foreach (var otherUrl in umbracoContext.UrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text).ThenBy(x => x.Culture)) if (urls.Add(otherUrl)) //avoid duplicates yield return otherUrl; @@ -119,7 +119,7 @@ private static IEnumerable GetContentUrlsByCulture(IContent content, switch (url) { - // deal with 'could not get the url' + // deal with 'could not get the URL' case "#": yield return HandleCouldNotGetUrl(content, culture, contentService, textService); break; @@ -129,7 +129,7 @@ private static IEnumerable GetContentUrlsByCulture(IContent content, yield return UrlInfo.Message(textService.Localize("content/getUrlException"), culture); break; - // got a url, deal with collisions, add url + // got a URL, deal with collisions, add URL default: if (DetectCollision(content, url, culture, umbracoContext, publishedRouter, textService, out var urlInfo)) // detect collisions, etc yield return urlInfo; @@ -142,7 +142,7 @@ private static IEnumerable GetContentUrlsByCulture(IContent content, private static UrlInfo HandleCouldNotGetUrl(IContent content, string culture, IContentService contentService, ILocalizedTextService textService) { - // document has a published version yet its url is "#" => a parent must be + // document has a published version yet its URL is "#" => a parent must be // unpublished, walk up the tree until we find it, and report. var parent = content; do @@ -163,7 +163,7 @@ private static UrlInfo HandleCouldNotGetUrl(IContent content, string culture, IC private static bool DetectCollision(IContent content, string url, string culture, UmbracoContext umbracoContext, IPublishedRouter publishedRouter, ILocalizedTextService textService, out UrlInfo urlInfo) { - // test for collisions on the 'main' url + // test for collisions on the 'main' URL var uri = new Uri(url.TrimEnd('/'), UriKind.RelativeOrAbsolute); if (uri.IsAbsoluteUri == false) uri = uri.MakeAbsolute(umbracoContext.CleanedUmbracoUrl); uri = UriUtility.UriToUmbraco(uri); diff --git a/src/Umbraco.Web/Runtime/WebInitialComponent.cs b/src/Umbraco.Web/Runtime/WebInitialComponent.cs index ac813d71969a..ee4bb86273d6 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComponent.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComponent.cs @@ -129,7 +129,7 @@ private static void ConfigureClientDependency(IGlobalSettings globalSettings) if (ConfigurationManager.GetSection("system.web/httpRuntime") is HttpRuntimeSection section) { - //set the max url length for CDF to be the smallest of the max query length, max request length + //set the max URL length for CDF to be the smallest of the max query length, max request length ClientDependency.Core.CompositeFiles.CompositeDependencyHandler.MaxHandlerUrlLength = Math.Min(section.MaxQueryStringLength, section.MaxRequestLength); } @@ -208,7 +208,7 @@ private static void RouteLocalApiController(Type controller, string umbracoPath) var url = umbracoPath + (meta.IsBackOffice ? "/BackOffice" : "") + "/Api/" + meta.ControllerName + "/{action}/{id}"; var route = RouteTable.Routes.MapHttpRoute( $"umbraco-api-{meta.ControllerName}", - url, // url to match + url, // URL to match new { controller = meta.ControllerName, id = UrlParameter.Optional }, new[] { meta.ControllerNamespace }); if (route.DataTokens == null) // web api routes don't set the data tokens object @@ -222,7 +222,7 @@ private static void RouteLocalSurfaceController(Type controller, string umbracoP var url = umbracoPath + "/Surface/" + meta.ControllerName + "/{action}/{id}"; var route = RouteTable.Routes.MapRoute( $"umbraco-surface-{meta.ControllerName}", - url, // url to match + url, // URL to match new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, new[] { meta.ControllerNamespace }); // look in this namespace to create the controller route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "surface"); // ensure the umbraco token is set diff --git a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs index dbd82cc89961..f353a9506e29 100644 --- a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs +++ b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs @@ -16,7 +16,7 @@ namespace Umbraco.Web.Scheduling { - internal sealed class SchedulerComponent : IComponent + public sealed class SchedulerComponent : IComponent { private const int DefaultDelayMilliseconds = 180000; // 3 mins private const int OneMinuteMilliseconds = 60000; diff --git a/src/Umbraco.Web/Scheduling/SchedulerComposer.cs b/src/Umbraco.Web/Scheduling/SchedulerComposer.cs index 5c56f3d314d6..8e71004d0c14 100644 --- a/src/Umbraco.Web/Scheduling/SchedulerComposer.cs +++ b/src/Umbraco.Web/Scheduling/SchedulerComposer.cs @@ -12,6 +12,6 @@ namespace Umbraco.Web.Scheduling /// the task correctly instead of killing it completely when the app domain shuts down. /// [RuntimeLevel(MinLevel = RuntimeLevel.Run)] - internal sealed class SchedulerComposer : ComponentComposer, ICoreComposer + public sealed class SchedulerComposer : ComponentComposer, ICoreComposer { } } diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs index 0a3e57c4fd93..8f33f10eea29 100644 --- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs @@ -1,5 +1,8 @@ using System; using System.Threading; +using System.Web; +using System.Web.Mvc; +using System.Web.SessionState; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; @@ -266,7 +269,7 @@ private static void UseUmbracoBackOfficeCookieAuthenticationInternal(this IAppBu { //Then our custom middlewares app.Use(typeof(ForceRenewalCookieAuthenticationMiddleware), app, options, Current.UmbracoContextAccessor); - app.Use(typeof(FixWindowsAuthMiddlware)); + app.Use(typeof(FixWindowsAuthMiddlware)); } //Marks all of the above middlewares to execute on Authenticate @@ -378,6 +381,19 @@ public static IAppBuilder UseUmbracoPreviewAuthentication(this IAppBuilder app, return app; } + /// + /// Enable the back office to detect and handle errors registered with external login providers + /// + /// + /// + /// + public static IAppBuilder UseUmbracoBackOfficeExternalLoginErrors(this IAppBuilder app, PipelineStage stage = PipelineStage.Authorize) + { + app.Use(typeof(BackOfficeExternalLoginProviderErrorMiddlware)); + app.UseStageMarker(stage); + return app; + } + public static void SanitizeThreadCulture(this IAppBuilder app) { Thread.CurrentThread.SanitizeThreadCulture(); diff --git a/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs b/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs index 5da9c77d6b8d..78ff55ba73ab 100644 --- a/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs +++ b/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs @@ -1,9 +1,14 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin.Security; +using Umbraco.Core; +using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Security { @@ -36,6 +41,36 @@ private static ExternalLoginInfo GetExternalLoginInfo(AuthenticateResult result) }; } + public static IEnumerable GetBackOfficeExternalLoginProviders(this IAuthenticationManager manager) + { + return manager.GetExternalAuthenticationTypes().Where(p => p.Properties.ContainsKey(Constants.Security.BackOfficeAuthenticationType)); + } + + /// + /// Returns the authentication type for the last registered external login (oauth) provider that specifies an auto-login redirect option + /// + /// + /// + public static string GetAutoLoginProvider(this IAuthenticationManager manager) + { + var found = manager.GetExternalAuthenticationTypes() + .LastOrDefault(p => p.Properties.ContainsKey(Constants.Security.BackOfficeAuthenticationType) + && p.Properties.TryGetValue(Constants.Security.BackOfficeExternalLoginOptionsProperty, out var options) + && options is BackOfficeExternalLoginProviderOptions externalLoginProviderOptions + && externalLoginProviderOptions.AutoRedirectLoginToExternalProvider); + + return found?.AuthenticationType; + } + + public static bool HasDenyLocalLogin(this IAuthenticationManager manager) + { + return manager.GetExternalAuthenticationTypes() + .Any(p => p.Properties.ContainsKey(Constants.Security.BackOfficeAuthenticationType) + && p.Properties.TryGetValue(Constants.Security.BackOfficeExternalLoginOptionsProperty, out var options) + && options is BackOfficeExternalLoginProviderOptions externalLoginProviderOptions + && externalLoginProviderOptions.DenyLocalLogin); + } + /// /// Extracts login info out of an external identity /// diff --git a/src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs b/src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs index 568bfc1a26b3..18f43b07e377 100644 --- a/src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs +++ b/src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs @@ -9,53 +9,59 @@ namespace Umbraco.Web.Security { public static class AuthenticationOptionsExtensions { + // these are used for backwards compat + private const string ExternalSignInAutoLinkOptionsProperty = "ExternalSignInAutoLinkOptions"; + private const string ChallengeResultCallbackProperty = "ChallengeResultCallback"; /// - /// When trying to implement an Azure AD B2C provider or other OAuth provider that requires a customized Challenge Result in order to work then - /// this must be used. + /// Used to specify all back office external login options /// /// - /// - /// - /// See: http://issues.umbraco.org/issue/U4-7353 - /// + /// + public static void SetBackOfficeExternalLoginProviderOptions(this AuthenticationOptions authOptions, + BackOfficeExternalLoginProviderOptions externalLoginProviderOptions) + { + authOptions.Description.Properties[Constants.Security.BackOfficeExternalLoginOptionsProperty] = externalLoginProviderOptions; + + // for backwards compat, we need to add these: + if (externalLoginProviderOptions.AutoLinkOptions != null) + authOptions.Description.Properties[ExternalSignInAutoLinkOptionsProperty] = externalLoginProviderOptions.AutoLinkOptions; + if (externalLoginProviderOptions.OnChallenge != null) + authOptions.Description.Properties[ChallengeResultCallbackProperty] = externalLoginProviderOptions.OnChallenge; + } + + [Obsolete("Use SetBackOfficeExternalLoginProviderOptions instead")] public static void SetSignInChallengeResultCallback( this AuthenticationOptions authOptions, Func authProperties) { - authOptions.Description.Properties["ChallengeResultCallback"] = authProperties; + authOptions.Description.Properties[ChallengeResultCallbackProperty] = authProperties; } public static AuthenticationProperties GetSignInChallengeResult(this AuthenticationDescription authenticationDescription, IOwinContext ctx) { - if (authenticationDescription.Properties.ContainsKey("ChallengeResultCallback") == false) return null; - var cb = authenticationDescription.Properties["ChallengeResultCallback"] as Func; + if (authenticationDescription.Properties.ContainsKey(ChallengeResultCallbackProperty) == false) return null; + var cb = authenticationDescription.Properties[ChallengeResultCallbackProperty] as Func; if (cb == null) return null; return cb(ctx); } - /// - /// Used during the External authentication process to assign external sign-in options - /// that are used by the Umbraco authentication process. - /// - /// - /// + [Obsolete("Use SetBackOfficeExternalLoginProviderOptions instead")] public static void SetExternalSignInAutoLinkOptions( this AuthenticationOptions authOptions, ExternalSignInAutoLinkOptions options) { - authOptions.Description.Properties["ExternalSignInAutoLinkOptions"] = options; + authOptions.Description.Properties[ExternalSignInAutoLinkOptionsProperty] = options; } - /// - /// Used during the External authentication process to retrieve external sign-in options - /// that have been set with SetExternalAuthenticationOptions - /// - /// + [Obsolete("Use GetExternalSignInAutoLinkOptions instead")] public static ExternalSignInAutoLinkOptions GetExternalAuthenticationOptions(this AuthenticationDescription authenticationDescription) + => authenticationDescription.GetExternalSignInAutoLinkOptions(); + + public static ExternalSignInAutoLinkOptions GetExternalSignInAutoLinkOptions(this AuthenticationDescription authenticationDescription) { - if (authenticationDescription.Properties.ContainsKey("ExternalSignInAutoLinkOptions") == false) return null; - var options = authenticationDescription.Properties["ExternalSignInAutoLinkOptions"] as ExternalSignInAutoLinkOptions; + if (authenticationDescription.Properties.ContainsKey(ExternalSignInAutoLinkOptionsProperty) == false) return null; + var options = authenticationDescription.Properties[ExternalSignInAutoLinkOptionsProperty] as ExternalSignInAutoLinkOptions; return options; } @@ -88,7 +94,7 @@ public static void ForUmbracoBackOffice(this AuthenticationOptions options, stri options.Description.Properties["SocialIcon"] = icon; //flag for use in back office - options.Description.Properties["UmbracoBackOffice"] = true; + options.Description.Properties[Constants.Security.BackOfficeAuthenticationType] = true; if (callbackPath.IsNullOrWhiteSpace()) { diff --git a/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs index fbc24d2cd9ef..d61d2ea711f8 100644 --- a/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs @@ -26,7 +26,13 @@ public BackOfficeClaimsIdentityFactory() public override async Task CreateAsync(UserManager manager, T user, string authenticationType) { var baseIdentity = await base.CreateAsync(manager, user, authenticationType); - + + // now we can flow any custom claims that the actual user has currently assigned which could be done in the OnExternalLogin callback + foreach (var claim in user.Claims) + { + baseIdentity.AddClaim(new Claim(claim.ClaimType, claim.ClaimValue)); + } + var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity, user.Id, user.UserName, diff --git a/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrorMiddlware.cs b/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrorMiddlware.cs new file mode 100644 index 000000000000..6e6477443bc0 --- /dev/null +++ b/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrorMiddlware.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Web.Mvc; +using Microsoft.Owin; +using Newtonsoft.Json; +using Umbraco.Core; + +namespace Umbraco.Web.Security +{ + /// + /// Used to handle errors registered by external login providers + /// + /// + /// When an external login provider registers an error with during the OAuth process, + /// this middleware will detect that, store the errors into cookie data and redirect to the back office login so we can read the errors back out. + /// + internal class BackOfficeExternalLoginProviderErrorMiddlware : OwinMiddleware + { + public BackOfficeExternalLoginProviderErrorMiddlware(OwinMiddleware next) : base(next) + { + } + + public override async Task Invoke(IOwinContext context) + { + var shortCircuit = false; + if (!context.Request.Uri.IsClientSideRequest()) + { + // check if we have any errors registered + var errors = context.GetExternalLoginProviderErrors(); + if (errors != null) + { + shortCircuit = true; + + var serialized = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(errors))); + + context.Response.Cookies.Append(ViewDataExtensions.TokenExternalSignInError, serialized, new CookieOptions + { + Expires = DateTime.Now.AddMinutes(5), + HttpOnly = true, + Secure = context.Request.IsSecure + }); + + context.Response.Redirect(context.Request.Uri.ToString()); + } + } + + if (Next != null && !shortCircuit) + { + await Next.Invoke(context); + } + } + } +} diff --git a/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrors.cs b/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrors.cs new file mode 100644 index 000000000000..39b967fa967a --- /dev/null +++ b/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrors.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Web.Security +{ + public class BackOfficeExternalLoginProviderErrors + { + // required for deserialization + public BackOfficeExternalLoginProviderErrors() + { + } + + public BackOfficeExternalLoginProviderErrors(string authenticationType, IEnumerable errors) + { + AuthenticationType = authenticationType; + Errors = errors ?? Enumerable.Empty(); + } + + public string AuthenticationType { get; set; } + public IEnumerable Errors { get; set; } + } +} diff --git a/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderOptions.cs b/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderOptions.cs new file mode 100644 index 000000000000..4ef527460e11 --- /dev/null +++ b/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderOptions.cs @@ -0,0 +1,53 @@ +using Microsoft.Owin; +using Microsoft.Owin.Security; +using System; +using System.Collections; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Security +{ + + /// + /// Options used to configure back office external login providers + /// + public class BackOfficeExternalLoginProviderOptions + { + /// + /// When specified this will be called to retrieve the used during the authentication Challenge response. + /// + /// + /// This will generally not be needed since OpenIdConnect.RedirectToIdentityProvider options should be used instead + /// + [IgnoreDataMember] + public Func OnChallenge { get; set; } + + /// + /// Options used to control how users can be auto-linked/created/updated based on the external login provider + /// + [IgnoreDataMember] // we are ignoring this one from serialization for backwards compat since these options are manually incuded in the response separately + public ExternalSignInAutoLinkOptions AutoLinkOptions { get; set; } = new ExternalSignInAutoLinkOptions(); + + /// + /// When set to true will disable all local user login functionality + /// + 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. + /// + /// + /// 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; 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 + /// + /// + /// 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; set; } + } +} diff --git a/src/Umbraco.Web/Security/BackOfficeUserManager.cs b/src/Umbraco.Web/Security/BackOfficeUserManager.cs index 6205c1705c9f..f91c128cde63 100644 --- a/src/Umbraco.Web/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web/Security/BackOfficeUserManager.cs @@ -610,9 +610,11 @@ internal void RaiseLoginSuccessEvent(int userId) OnLoginSuccess(new IdentityAuditEventArgs(AuditEvent.LoginSucces, GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseLogoutSuccessEvent(int userId) + internal SignOutAuditEventArgs RaiseLogoutSuccessEvent(int userId) { - OnLogoutSuccess(new IdentityAuditEventArgs(AuditEvent.LogoutSuccess, GetCurrentRequestIpAddress(), affectedUser: userId)); + var args = new SignOutAuditEventArgs(AuditEvent.LogoutSuccess, GetCurrentRequestIpAddress(), affectedUser: userId); + OnLogoutSuccess(args); + return args; } internal void RaisePasswordChangedEvent(int userId) @@ -631,6 +633,10 @@ internal void RaiseResetAccessFailedCountEvent(int userId) OnResetAccessFailedCount(new IdentityAuditEventArgs(AuditEvent.ResetAccessFailedCount, GetCurrentRequestIpAddress(), affectedUser: userId)); } + internal void RaiseSendingUserInvite(UserInviteEventArgs args) => OnSendingUserInvite(args); + internal bool HasSendingUserInviteEventHandler => SendingUserInvite != null; + + // TODO: Not sure why these are not strongly typed events?? They should be in netcore! public static event EventHandler AccountLocked; public static event EventHandler AccountUnlocked; public static event EventHandler ForgotPasswordRequested; @@ -643,60 +649,34 @@ internal void RaiseResetAccessFailedCountEvent(int userId) public static event EventHandler PasswordReset; public static event EventHandler ResetAccessFailedCount; - protected virtual void OnAccountLocked(IdentityAuditEventArgs e) - { - if (AccountLocked != null) AccountLocked(this, e); - } + /// + /// Raised when a user is invited + /// + public static event EventHandler SendingUserInvite; // this event really has nothing to do with the user manager but was the most convenient place to put it - protected virtual void OnAccountUnlocked(IdentityAuditEventArgs e) - { - if (AccountUnlocked != null) AccountUnlocked(this, e); - } + protected virtual void OnSendingUserInvite(UserInviteEventArgs e) => SendingUserInvite?.Invoke(this, e); - protected virtual void OnForgotPasswordRequested(IdentityAuditEventArgs e) - { - if (ForgotPasswordRequested != null) ForgotPasswordRequested(this, e); - } + protected virtual void OnAccountLocked(IdentityAuditEventArgs e) => AccountLocked?.Invoke(this, e); - protected virtual void OnForgotPasswordChangedSuccess(IdentityAuditEventArgs e) - { - if (ForgotPasswordChangedSuccess != null) ForgotPasswordChangedSuccess(this, e); - } + protected virtual void OnAccountUnlocked(IdentityAuditEventArgs e) => AccountUnlocked?.Invoke(this, e); - protected virtual void OnLoginFailed(IdentityAuditEventArgs e) - { - if (LoginFailed != null) LoginFailed(this, e); - } + protected virtual void OnForgotPasswordRequested(IdentityAuditEventArgs e) => ForgotPasswordRequested?.Invoke(this, e); - protected virtual void OnLoginRequiresVerification(IdentityAuditEventArgs e) - { - if (LoginRequiresVerification != null) LoginRequiresVerification(this, e); - } + protected virtual void OnForgotPasswordChangedSuccess(IdentityAuditEventArgs e) => ForgotPasswordChangedSuccess?.Invoke(this, e); - protected virtual void OnLoginSuccess(IdentityAuditEventArgs e) - { - if (LoginSuccess != null) LoginSuccess(this, e); - } + protected virtual void OnLoginFailed(IdentityAuditEventArgs e) => LoginFailed?.Invoke(this, e); - protected virtual void OnLogoutSuccess(IdentityAuditEventArgs e) - { - if (LogoutSuccess != null) LogoutSuccess(this, e); - } + protected virtual void OnLoginRequiresVerification(IdentityAuditEventArgs e) => LoginRequiresVerification?.Invoke(this, e); - protected virtual void OnPasswordChanged(IdentityAuditEventArgs e) - { - if (PasswordChanged != null) PasswordChanged(this, e); - } + protected virtual void OnLoginSuccess(IdentityAuditEventArgs e) => LoginSuccess?.Invoke(this, e); - protected virtual void OnPasswordReset(IdentityAuditEventArgs e) - { - if (PasswordReset != null) PasswordReset(this, e); - } + protected virtual void OnLogoutSuccess(IdentityAuditEventArgs e) => LogoutSuccess?.Invoke(this, e); - protected virtual void OnResetAccessFailedCount(IdentityAuditEventArgs e) - { - if (ResetAccessFailedCount != null) ResetAccessFailedCount(this, e); - } + protected virtual void OnPasswordChanged(IdentityAuditEventArgs e) => PasswordChanged?.Invoke(this, e); + + protected virtual void OnPasswordReset(IdentityAuditEventArgs e) => PasswordReset?.Invoke(this, e); + + protected virtual void OnResetAccessFailedCount(IdentityAuditEventArgs e) => ResetAccessFailedCount?.Invoke(this, e); /// /// Returns the current request IP address for logging if there is one diff --git a/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs index 2a9c3a5f07c9..52f758aa74d9 100644 --- a/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Serialization; using Microsoft.AspNet.Identity.Owin; using Umbraco.Core; using Umbraco.Core.Composing; @@ -7,6 +8,7 @@ namespace Umbraco.Web.Security { + /// /// Options used to configure auto-linking external OAuth providers /// @@ -30,15 +32,23 @@ public ExternalSignInAutoLinkOptions( private readonly string[] _defaultUserGroups; + /// + /// By default this is true which allows the user to manually link and unlink the external provider, if set to false the back office user + /// will not see and cannot perform manual linking or unlinking of the external provider. + /// + public bool AllowManualLinking { get; set; } = true; + /// /// A callback executed during account auto-linking and before the user is persisted /// + [IgnoreDataMember] public Action OnAutoLinking { get; set; } /// /// A callback executed during every time a user authenticates using an external login. /// returns a boolean indicating if sign in should continue or not. /// + [IgnoreDataMember] public Func OnExternalLogin { get; set; } @@ -48,7 +58,7 @@ public ExternalSignInAutoLinkOptions( /// /// /// - public string[] GetDefaultUserGroups(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + public virtual string[] GetDefaultUserGroups(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) { return _defaultUserGroups; } @@ -61,7 +71,7 @@ public string[] GetDefaultUserGroups(UmbracoContext umbracoContext, ExternalLogi /// /// For public auth providers this should always be false!!! /// - public bool ShouldAutoLinkExternalAccount(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + public virtual bool ShouldAutoLinkExternalAccount(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) { return _autoLinkExternalAccount; } @@ -71,7 +81,7 @@ public bool ShouldAutoLinkExternalAccount(UmbracoContext umbracoContext, Externa /// /// The default Culture to use for auto-linking users /// - public string GetDefaultCulture(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + public virtual string GetDefaultCulture(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) { return _defaultCulture; } diff --git a/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs b/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs index 30038e1f31a1..3338344e733b 100644 --- a/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs +++ b/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs @@ -8,6 +8,7 @@ namespace Umbraco.Web.Security { + /// /// This is used to inspect the request to see if 2 x identities are assigned: A windows one and a back office one. /// When this is the case, it means that auth has executed for Windows & auth has executed for our back office cookie diff --git a/src/Umbraco.Web/Security/IdentityAuditEventArgs.cs b/src/Umbraco.Web/Security/IdentityAuditEventArgs.cs index 81407afe503b..23e570dd401c 100644 --- a/src/Umbraco.Web/Security/IdentityAuditEventArgs.cs +++ b/src/Umbraco.Web/Security/IdentityAuditEventArgs.cs @@ -4,6 +4,7 @@ namespace Umbraco.Web.Security { + /// /// This class is used by events raised from the BackofficeUserManager /// @@ -126,6 +127,7 @@ public enum AuditEvent LogoutSuccess, PasswordChanged, PasswordReset, - ResetAccessFailedCount + ResetAccessFailedCount, + SendingUserInvite } } diff --git a/src/Umbraco.Web/Security/SignOutAuditEventArgs.cs b/src/Umbraco.Web/Security/SignOutAuditEventArgs.cs new file mode 100644 index 000000000000..e7943f70b66d --- /dev/null +++ b/src/Umbraco.Web/Security/SignOutAuditEventArgs.cs @@ -0,0 +1,19 @@ +namespace Umbraco.Web.Security +{ + /// + /// Event args used when signing out + /// + public class SignOutAuditEventArgs : IdentityAuditEventArgs + { + public SignOutAuditEventArgs(AuditEvent action, string ipAddress, string comment = null, int performingUser = -1, int affectedUser = -1) + : base(action, ipAddress, comment, performingUser, affectedUser) + { + } + + /// + /// Allows event handlers to set a GET absolute URL to be redirected to after successful logout out of the back office. This + /// can be used for external login providers. + /// + public string SignOutRedirectUrl { get; set; } + } +} diff --git a/src/Umbraco.Web/Security/UserInviteEventArgs.cs b/src/Umbraco.Web/Security/UserInviteEventArgs.cs new file mode 100644 index 000000000000..9fb53a44c053 --- /dev/null +++ b/src/Umbraco.Web/Security/UserInviteEventArgs.cs @@ -0,0 +1,36 @@ +using Umbraco.Core.Models.Membership; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Security +{ + public class UserInviteEventArgs : IdentityAuditEventArgs + { + public UserInviteEventArgs(string ipAddress, int performingUser, UserInvite invitedUser, IUser localUser, string comment = null) + : base(AuditEvent.SendingUserInvite, ipAddress, comment, performingUser) + { + InvitedUser = invitedUser ?? throw new System.ArgumentNullException(nameof(invitedUser)); + User = localUser; + } + + /// + /// The model used to invite the user + /// + public UserInvite InvitedUser { get; } + + /// + /// If event handler sets this to true it indicates that Umbraco will no try to send the invite itself + /// + public bool InviteHandled { get; set; } + + /// + /// The local user that has been created that is pending the invite + /// + public IUser User { get; } + + /// + /// if set to true will show the edit user button in the UI, else it will not be shown + /// + public bool ShowUserResult { get; set; } + + } +} diff --git a/src/Umbraco.Web/Services/IconService.cs b/src/Umbraco.Web/Services/IconService.cs index 206af296f97e..ba6692c7b9f3 100644 --- a/src/Umbraco.Web/Services/IconService.cs +++ b/src/Umbraco.Web/Services/IconService.cs @@ -19,13 +19,11 @@ public IconService(IGlobalSettings globalSettings) _globalSettings = globalSettings; } - /// public IList GetAllIcons() { var icons = new List(); - var directory = new DirectoryInfo(IOHelper.MapPath($"{_globalSettings.IconsPath}/")); - var iconNames = directory.GetFiles("*.svg"); + var iconNames = GetAllIconNames(); iconNames.OrderBy(f => f.Name).ToList().ForEach(iconInfo => { @@ -45,36 +43,56 @@ public IconModel GetIcon(string iconName) { return string.IsNullOrWhiteSpace(iconName) ? null - : CreateIconModel(iconName.StripFileExtension(), IOHelper.MapPath($"{_globalSettings.IconsPath}/{iconName}.svg")); + : CreateIconModel(iconName.StripFileExtension()); } /// /// Gets an IconModel using values from a FileInfo model /// /// - /// - private IconModel GetIcon(FileInfo fileInfo) + /// + private IconModel GetIcon(FileSystemInfo fileInfo) { return fileInfo == null || string.IsNullOrWhiteSpace(fileInfo.Name) ? null : CreateIconModel(fileInfo.Name.StripFileExtension(), fileInfo.FullName); } + /// + /// Gets an IconModel containing the icon name and SvgString + /// + /// + /// + private IconModel CreateIconModel(string iconName) + { + if (string.IsNullOrWhiteSpace(iconName)) + return null; + + var iconNames = GetAllIconNames(); + var iconPath = iconNames.FirstOrDefault(x => x.Name.InvariantEquals($"{iconName}.svg"))?.FullName; + return iconPath == null + ? null + : CreateIconModel(iconName, iconPath); + } + /// /// Gets an IconModel containing the icon name and SvgString /// /// /// - /// - private IconModel CreateIconModel(string iconName, string iconPath) + /// + private static IconModel CreateIconModel(string iconName, string iconPath) { - var sanitizer = new HtmlSanitizer(); - sanitizer.AllowedAttributes.UnionWith(Constants.SvgSanitizer.Attributes); - sanitizer.AllowedCssProperties.UnionWith(Constants.SvgSanitizer.Attributes); - sanitizer.AllowedTags.UnionWith(Constants.SvgSanitizer.Tags); + if (string.IsNullOrWhiteSpace(iconPath)) + return null; try { + var sanitizer = new HtmlSanitizer(); + sanitizer.AllowedAttributes.UnionWith(Constants.SvgSanitizer.Attributes); + sanitizer.AllowedCssProperties.UnionWith(Constants.SvgSanitizer.Attributes); + sanitizer.AllowedTags.UnionWith(Constants.SvgSanitizer.Tags); + var svgContent = System.IO.File.ReadAllText(iconPath); var sanitizedString = sanitizer.Sanitize(svgContent); @@ -91,5 +109,26 @@ private IconModel CreateIconModel(string iconName, string iconPath) return null; } } + + private IEnumerable GetAllIconNames() + { + // add icons from plugins + var appPlugins = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.AppPlugins)); + var pluginIcons = appPlugins.Exists == false + ? new List() + : appPlugins.GetDirectories() + // Find all directories in App_Plugins that are named "Icons" and get a list of SVGs from them + .SelectMany(x => x.GetDirectories("Icons", SearchOption.AllDirectories)) + .SelectMany(x => x.GetFiles("*.svg", SearchOption.TopDirectoryOnly)); + + // add icons from IconsPath if not already added from plugins + var directory = new DirectoryInfo(IOHelper.MapPath($"{_globalSettings.IconsPath}/")); + var iconNames = directory.GetFiles("*.svg") + .Where(x => pluginIcons.Any(i => i.Name == x.Name) == false); + + iconNames = iconNames.Concat(pluginIcons).ToList(); + + return iconNames; + } } } diff --git a/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs index 539ce303fd91..4fa77be77f03 100644 --- a/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs +++ b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs @@ -82,7 +82,7 @@ public string EnsureImageSources(string text) } /// - /// Removes media urls from <img> tags where a data-udi attribute is present + /// Removes media URLs from <img> tags where a data-udi attribute is present /// /// /// diff --git a/src/Umbraco.Web/Templates/HtmlLocalLinkParser.cs b/src/Umbraco.Web/Templates/HtmlLocalLinkParser.cs index f65a7183b751..391614e68ef8 100644 --- a/src/Umbraco.Web/Templates/HtmlLocalLinkParser.cs +++ b/src/Umbraco.Web/Templates/HtmlLocalLinkParser.cs @@ -47,7 +47,7 @@ public string EnsureInternalLinks(string text, bool preview) if (!preview) return EnsureInternalLinks(text); - using (_umbracoContextAccessor.UmbracoContext.ForcedPreview(preview)) // force for url provider + using (_umbracoContextAccessor.UmbracoContext.ForcedPreview(preview)) // force for URL provider { return EnsureInternalLinks(text); } diff --git a/src/Umbraco.Web/Templates/HtmlUrlParser.cs b/src/Umbraco.Web/Templates/HtmlUrlParser.cs index 5b78477579ed..0692b75fa7ec 100644 --- a/src/Umbraco.Web/Templates/HtmlUrlParser.cs +++ b/src/Umbraco.Web/Templates/HtmlUrlParser.cs @@ -34,7 +34,7 @@ public string EnsureUrls(string text) using (var timer = _logger.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete")) { - // find all relative urls (ie. urls that contain ~) + // find all relative URLs (ie. URLs that contain ~) var tags = ResolveUrlPattern.Matches(text); _logger.Debug(typeof(IOHelper), "After regex: {Duration} matched: {TagsCount}", timer.Stopwatch.ElapsedMilliseconds, tags.Count); foreach (Match tag in tags) @@ -43,7 +43,7 @@ public string EnsureUrls(string text) if (tag.Groups[1].Success) url = tag.Groups[1].Value; - // The richtext editor inserts a slash in front of the url. That's why we need this little fix + // The richtext editor inserts a slash in front of the URL. That's why we need this little fix // if (url.StartsWith("/")) // text = text.Replace(url, ResolveUrl(url.Substring(1))); // else diff --git a/src/Umbraco.Web/Templates/TemplateRenderer.cs b/src/Umbraco.Web/Templates/TemplateRenderer.cs index 4602942be16c..127dc6033ad6 100644 --- a/src/Umbraco.Web/Templates/TemplateRenderer.cs +++ b/src/Umbraco.Web/Templates/TemplateRenderer.cs @@ -45,7 +45,7 @@ public void Render(int pageId, int? altTemplateId, StringWriter writer) if (writer == null) throw new ArgumentNullException(nameof(writer)); // instantiate a request and process - // important to use CleanedUmbracoUrl - lowercase path-only version of the current url, though this isn't going to matter + // important to use CleanedUmbracoUrl - lowercase path-only version of the current URL, though this isn't going to matter // terribly much for this implementation since we are just creating a doc content request to modify it's properties manually. var contentRequest = _publishedRouter.CreateRequest(_umbracoContextAccessor.UmbracoContext); diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index f08abfaf12a8..105d3e0812e0 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -22,7 +22,7 @@ public static class TemplateUtilities [Obsolete("Inject and use an instance of " + nameof(HtmlLocalLinkParser) + " instead")] internal static string ParseInternalLinks(string text, bool preview, UmbracoContext umbracoContext) { - using (umbracoContext.ForcedPreview(preview)) // force for url provider + using (umbracoContext.ForcedPreview(preview)) // force for URL provider { text = ParseInternalLinks(text, umbracoContext.UrlProvider); } diff --git a/src/Umbraco.Web/Trees/DictionaryTreeController.cs b/src/Umbraco.Web/Trees/DictionaryTreeController.cs index 3f7b89bcdfb1..f21f0b2868e2 100644 --- a/src/Umbraco.Web/Trees/DictionaryTreeController.cs +++ b/src/Umbraco.Web/Trees/DictionaryTreeController.cs @@ -72,7 +72,7 @@ protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection } else { - // maybe we should use the guid as url param to avoid the extra call for getting dictionary item + // maybe we should use the guid as URL param to avoid the extra call for getting dictionary item var parentDictionary = Services.LocalizationService.GetDictionaryItemById(intId.Result); if (parentDictionary == null) return nodes; diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 77e5ebef6223..f618d053ac08 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -250,7 +250,7 @@ public TreeNode CreateTreeNode(string id, string parentId, FormDataCollection qu } /// - /// Helper method to create tree nodes and automatically generate the json url + UDI + /// Helper method to create tree nodes and automatically generate the json URL + UDI /// /// /// @@ -270,7 +270,7 @@ public TreeNode CreateTreeNode(IEntitySlim entity, Guid entityObjectType, string } /// - /// Helper method to create tree nodes and automatically generate the json url + UDI + /// Helper method to create tree nodes and automatically generate the json URL + UDI /// /// /// @@ -289,7 +289,7 @@ public TreeNode CreateTreeNode(IUmbracoEntity entity, Guid entityObjectType, str } /// - /// Helper method to create tree nodes and automatically generate the json url + /// Helper method to create tree nodes and automatically generate the json URL /// /// /// @@ -306,7 +306,7 @@ public TreeNode CreateTreeNode(string id, string parentId, FormDataCollection qu } /// - /// Helper method to create tree nodes and automatically generate the json url + /// Helper method to create tree nodes and automatically generate the json URL /// /// /// @@ -325,7 +325,7 @@ public TreeNode CreateTreeNode(string id, string parentId, FormDataCollection qu } /// - /// Helper method to create tree nodes and automatically generate the json url + UDI + /// Helper method to create tree nodes and automatically generate the json URL + UDI /// /// /// diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index c3eba87d6f95..1b48b9ca0d37 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -159,8 +159,10 @@ + + @@ -283,6 +285,11 @@ + + + + + diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index 347f79e51b55..ab8470c64f08 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -66,7 +66,7 @@ internal UmbracoContext(HttpContextBase httpContext, // beware - we cannot expect a current user here, so detecting preview mode must be a lazy thing _publishedSnapshot = new Lazy(() => publishedSnapshotService.CreatePublishedSnapshot(PreviewToken)); - // set the urls... + // set the URLs... // NOTE: The request will not be available during app startup so we can only set this to an absolute URL of localhost, this // is a work around to being able to access the UmbracoContext during application startup and this will also ensure that people // 'could' still generate URLs during startup BUT any domain driven URL generation will not work because it is NOT possible to get @@ -101,7 +101,7 @@ internal UmbracoContext(HttpContextBase httpContext, internal Uri OriginalRequestUrl { get; } /// - /// Gets the cleaned up url that is handled by Umbraco. + /// Gets the cleaned up URL that is handled by Umbraco. /// /// That is, lowercase, no trailing slash after path, no .aspx... internal Uri CleanedUmbracoUrl { get; } @@ -144,7 +144,7 @@ internal UmbracoContext(HttpContextBase httpContext, public bool IsFrontEndUmbracoRequest => PublishedRequest != null; /// - /// Gets the url provider. + /// Gets the URL provider. /// public UrlProvider UrlProvider { get; } @@ -197,57 +197,57 @@ public bool InPreviewMode #region Urls /// - /// Gets the url of a content identified by its identifier. + /// Gets the URL of a content identified by its identifier. /// /// The content identifier. /// - /// The url for the content. + /// The URL for the content. public string Url(int contentId, string culture = null) { return UrlProvider.GetUrl(contentId, culture: culture); } /// - /// Gets the url of a content identified by its identifier. + /// Gets the URL of a content identified by its identifier. /// /// The content identifier. /// - /// The url for the content. + /// The URL for the content. public string Url(Guid contentId, string culture = null) { return UrlProvider.GetUrl(contentId, culture: culture); } /// - /// Gets the url of a content identified by its identifier, in a specified mode. + /// Gets the URL of a content identified by its identifier, in a specified mode. /// /// The content identifier. /// The mode. /// - /// The url for the content. + /// The URL for the content. public string Url(int contentId, UrlMode mode, string culture = null) { return UrlProvider.GetUrl(contentId, mode, culture); } /// - /// Gets the url of a content identified by its identifier, in a specified mode. + /// Gets the URL of a content identified by its identifier, in a specified mode. /// /// The content identifier. /// The mode. /// - /// The url for the content. + /// The URL for the content. public string Url(Guid contentId, UrlMode mode, string culture = null) { return UrlProvider.GetUrl(contentId, mode, culture); } /// - /// Gets the absolute url of a content identified by its identifier. + /// Gets the absolute URL of a content identified by its identifier. /// /// The content identifier. /// - /// The absolute url for the content. + /// The absolute URL for the content. [Obsolete("Use the Url() method with UrlMode.Absolute.")] public string UrlAbsolute(int contentId, string culture = null) { @@ -255,11 +255,11 @@ public string UrlAbsolute(int contentId, string culture = null) } /// - /// Gets the absolute url of a content identified by its identifier. + /// Gets the absolute URL of a content identified by its identifier. /// /// The content identifier. /// - /// The absolute url for the content. + /// The absolute URL for the content. [Obsolete("Use the Url() method with UrlMode.Absolute.")] public string UrlAbsolute(Guid contentId, string culture = null) { diff --git a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs index 0633cca3a01e..9a9f5e7e0128 100644 --- a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs +++ b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs @@ -68,6 +68,9 @@ protected virtual void ConfigureMiddleware(IAppBuilder app) // Configure OWIN for authentication. ConfigureUmbracoAuthentication(app); + // must come after all authentication + app.UseUmbracoBackOfficeExternalLoginErrors(); + app .UseSignalR(GlobalSettings) .FinalizeMiddlewareConfiguration(); @@ -99,6 +102,7 @@ protected virtual void ConfigureUmbracoAuthentication(IAppBuilder app) app .UseUmbracoBackOfficeCookieAuthentication(UmbracoContextAccessor, RuntimeState, Services.UserService, GlobalSettings, UmbracoSettings.Security, PipelineStage.Authenticate) .UseUmbracoBackOfficeExternalCookieAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, PipelineStage.Authenticate) + // TODO: this would be considered a breaking change but this must come after all authentication so should be moved within ConfigureMiddleware .UseUmbracoPreviewAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, UmbracoSettings.Security, PipelineStage.Authorize); } diff --git a/src/Umbraco.Web/UmbracoHttpHandler.cs b/src/Umbraco.Web/UmbracoHttpHandler.cs index 447bdde43022..fae2b571c5ab 100644 --- a/src/Umbraco.Web/UmbracoHttpHandler.cs +++ b/src/Umbraco.Web/UmbracoHttpHandler.cs @@ -61,7 +61,7 @@ protected UmbracoHttpHandler(IUmbracoContextAccessor umbracoContextAccessor, Umb public WebSecurity Security => UmbracoContextAccessor.UmbracoContext.Security; /// - /// Gets the Url helper. + /// Gets the URL helper. /// /// This URL helper is created without any route data and an empty request context. public UrlHelper Url => _url ?? (_url = new UrlHelper(HttpContext.Current.Request.RequestContext)); diff --git a/src/Umbraco.Web/UmbracoInjectedModule.cs b/src/Umbraco.Web/UmbracoInjectedModule.cs index 6dc41d39eeac..6eeef081a2be 100644 --- a/src/Umbraco.Web/UmbracoInjectedModule.cs +++ b/src/Umbraco.Web/UmbracoInjectedModule.cs @@ -62,7 +62,7 @@ public UmbracoInjectedModule( /// private void BeginRequest(HttpContextBase httpContext) { - // ensure application url is initialized + // ensure application URL is initialized ((RuntimeState) Current.RuntimeState).EnsureApplicationUrl(httpContext.Request); // do not process if client-side request @@ -128,7 +128,7 @@ void ProcessRequest(HttpContextBase httpContext) // auth. Paul Sterling confirmed in Jan. 2013 that we can get rid of it. // instantiate, prepare and process the published content request - // important to use CleanedUmbracoUrl - lowercase path-only version of the current url + // important to use CleanedUmbracoUrl - lowercase path-only version of the current URL var request = _publishedRouter.CreateRequest(umbracoContext); umbracoContext.PublishedRequest = request; _publishedRouter.PrepareRequest(request); @@ -276,7 +276,7 @@ private void RewriteToUmbracoHandler(HttpContextBase context, PublishedRequest p { // NOTE: we do not want to use TransferRequest even though many docs say it is better with IIS7, turns out this is // not what we need. The purpose of TransferRequest is to ensure that .net processes all of the rules for the newly - // rewritten url, but this is not what we want! + // rewritten URL, but this is not what we want! // read: http://forums.iis.net/t/1146511.aspx var query = pcr.Uri.Query.TrimStart('?'); diff --git a/src/Umbraco.Web/UmbracoWebService.cs b/src/Umbraco.Web/UmbracoWebService.cs index bccdb5771d0e..f7268283f5ef 100644 --- a/src/Umbraco.Web/UmbracoWebService.cs +++ b/src/Umbraco.Web/UmbracoWebService.cs @@ -77,7 +77,7 @@ protected UmbracoWebService() public WebSecurity Security => UmbracoContext.Security; /// - /// Gets the Url helper. + /// Gets the URL helper. /// /// This URL helper is created without any route data and an empty request context. public UrlHelper Url => _url ?? (_url = new UrlHelper(Context.Request.RequestContext)); diff --git a/src/Umbraco.Web/UriUtility.cs b/src/Umbraco.Web/UriUtility.cs index 9e94a4a20a36..ae0b00191762 100644 --- a/src/Umbraco.Web/UriUtility.cs +++ b/src/Umbraco.Web/UriUtility.cs @@ -227,7 +227,7 @@ public static string TrimPathEndSlash(string uri) #endregion /// - /// Returns an full url with the host, port, etc... + /// Returns an full URL with the host, port, etc... /// /// An absolute path (i.e. starts with a '/' ) /// diff --git a/src/Umbraco.Web/UrlHelperExtensions.cs b/src/Umbraco.Web/UrlHelperExtensions.cs index 7e63ff16b815..f2c954d399c5 100644 --- a/src/Umbraco.Web/UrlHelperExtensions.cs +++ b/src/Umbraco.Web/UrlHelperExtensions.cs @@ -20,7 +20,7 @@ public static class UrlHelperExtensions { /// - /// Return the Url for a Web Api service + /// Return the URL for a Web Api service /// /// /// @@ -58,7 +58,7 @@ public static string GetUmbracoApiService(this UrlHelper url, Expression - /// Return the Url for a Web Api service + /// Return the URL for a Web Api service /// /// /// @@ -87,7 +87,7 @@ public static string GetUmbracoApiService(this UrlHelper url, string actionName, } /// - /// Return the Url for a Web Api service + /// Return the URL for a Web Api service /// /// /// @@ -119,7 +119,7 @@ public static string GetUmbracoApiService(this UrlHelper url, string actionName, /// - /// Return the Url for an action with a cache-busting hash appended + /// Return the URL for an action with a cache-busting hash appended /// /// /// diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index 8e410bf5846c..0f5b0557f4a1 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -23,7 +23,7 @@ public static class UrlHelperRenderExtensions #region GetCropUrl /// - /// Gets the ImageProcessor Url of a media item by the crop alias (using default media item property alias of "umbracoFile") + /// Gets the ImageProcessor URL of a media item by the crop alias (using default media item property alias of "umbracoFile") /// /// /// @@ -46,7 +46,7 @@ public static IHtmlString GetCropUrl(this UrlHelper urlHelper, IPublishedContent } /// - /// Gets the ImageProcessor Url by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. + /// Gets the ImageProcessor URL by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. /// /// /// @@ -63,7 +63,7 @@ public static IHtmlString GetCropUrl(this UrlHelper urlHelper, IPublishedContent /// set to false if using the result of this method for CSS. /// /// - /// The ImageProcessor.Web Url. + /// The ImageProcessor.Web URL. /// public static IHtmlString GetCropUrl(this UrlHelper urlHelper, IPublishedContent mediaItem, string propertyAlias, string cropAlias, bool htmlEncode = true) { @@ -74,7 +74,7 @@ public static IHtmlString GetCropUrl(this UrlHelper urlHelper, IPublishedContent } /// - /// Gets the ImageProcessor Url from the image path. + /// Gets the ImageProcessor URL from the image path. /// /// /// The IPublishedContent item. @@ -157,10 +157,10 @@ public static IHtmlString GetCropUrl(this UrlHelper urlHelper, } /// - /// Gets the ImageProcessor Url from the image path. + /// Gets the ImageProcessor URL from the image path. /// /// - /// The image url. + /// The image URL. /// /// /// The width of the output image. diff --git a/src/Umbraco.Web/ViewDataExtensions.cs b/src/Umbraco.Web/ViewDataExtensions.cs index e47171f1ba4d..ac4f4cdf755b 100644 --- a/src/Umbraco.Web/ViewDataExtensions.cs +++ b/src/Umbraco.Web/ViewDataExtensions.cs @@ -1,5 +1,12 @@ -using System.Collections.Generic; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Web; using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Web.Security; namespace Umbraco.Web { @@ -18,6 +25,49 @@ public static bool FromTempData(this ViewDataDictionary viewData, TempDataDictio return true; } + /// + /// Copies data from a request cookie to view data and then clears the cookie in the response + /// + /// + /// + /// + /// + /// + /// + /// This is similar to TempData but in some cases we cannot use TempData which relies on the temp data provider and session. + /// The cookie value can either be a simple string value + /// + /// + internal static bool FromBase64CookieData(this ViewDataDictionary viewData, HttpContextBase httpContext, string cookieName) + { + var hasCookie = httpContext.Request.HasCookie(cookieName); + if (!hasCookie) return false; + + // get the cookie value + var cookieVal = httpContext.Request.GetCookieValue(cookieName); + + if (cookieVal == null) + return false; + + // ensure the cookie is expired (must be done after reading the value) + httpContext.ExpireCookie(cookieName); + + if (cookieVal.IsNullOrWhiteSpace()) + return false; + + try + { + var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(System.Net.WebUtility.UrlDecode(cookieVal))); + // deserialize to T and store in viewdata + viewData[cookieName] = JsonConvert.DeserializeObject(decoded); + return true; + } + catch (Exception) + { + return false; + } + } + public static string GetUmbracoPath(this ViewDataDictionary viewData) { return (string)viewData[TokenUmbracoPath]; @@ -48,14 +98,37 @@ public static void SetUmbracoBaseFolder(this ViewDataDictionary viewData, string viewData[TokenUmbracoBaseFolder] = value; } + /// + /// Used by the back office login screen to get any registered external login provider errors + /// + /// + /// + public static BackOfficeExternalLoginProviderErrors GetExternalSignInProviderErrors(this ViewDataDictionary viewData) + { + return (BackOfficeExternalLoginProviderErrors)viewData[TokenExternalSignInError]; + } + + [Obsolete("Use GetExternalSignInProviderErrors instead")] public static IEnumerable GetExternalSignInError(this ViewDataDictionary viewData) { - return (IEnumerable)viewData[TokenExternalSignInError]; + var errs = viewData.GetExternalSignInProviderErrors(); + return errs?.Errors ?? Enumerable.Empty(); + } + + /// + /// Used by the back office controller to register any external login provider errors + /// + /// + /// + public static void SetExternalSignInProviderErrors(this ViewDataDictionary viewData, BackOfficeExternalLoginProviderErrors errors) + { + viewData[TokenExternalSignInError] = errors; } + [Obsolete("Use SetExternalSignInProviderErrors instead")] public static void SetExternalSignInError(this ViewDataDictionary viewData, IEnumerable value) { - viewData[TokenExternalSignInError] = value; + viewData[TokenExternalSignInError] = new BackOfficeExternalLoginProviderErrors(string.Empty, value); } public static string GetPasswordResetCode(this ViewDataDictionary viewData) diff --git a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs index a1cd7708e28c..9e22928cf621 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs @@ -125,7 +125,7 @@ protected UmbracoApiControllerBase(IGlobalSettings globalSettings, IUmbracoConte internal IRuntimeState RuntimeState { get; } /// - /// Gets the application url. + /// Gets the application URL. /// protected Uri ApplicationUrl => RuntimeState.ApplicationUrl;