diff --git a/src/Umbraco.Core/FactoryExtensions.cs b/src/Umbraco.Core/FactoryExtensions.cs index 8514525417..8ae2f76af3 100644 --- a/src/Umbraco.Core/FactoryExtensions.cs +++ b/src/Umbraco.Core/FactoryExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using Umbraco.Core.Composing; @@ -77,15 +78,28 @@ namespace Umbraco.Core var ctorParameters = ctor.GetParameters(); var ctorArgs = new object[ctorParameters.Length]; + var availableArgs = new List(args); var i = 0; foreach (var parameter in ctorParameters) { // no! IsInstanceOfType is not ok here // ReSharper disable once UseMethodIsInstanceOfType - var arg = args?.FirstOrDefault(a => parameter.ParameterType.IsAssignableFrom(a.GetType())); - ctorArgs[i++] = arg ?? factory.GetInstance(parameter.ParameterType); + var idx = availableArgs.FindIndex(a => parameter.ParameterType.IsAssignableFrom(a.GetType())); + if(idx >= 0) + { + // Found a suitable supplied argument + ctorArgs[i++] = availableArgs[idx]; + + // A supplied argument can be used at most once + availableArgs.RemoveAt(idx); + } + else + { + // None of the provided arguments is suitable: get an instance from the factory + ctorArgs[i++] = factory.GetInstance(parameter.ParameterType); + } } return ctor.Invoke(ctorArgs); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs index 225e29a8a1..f27feba8cf 100644 --- a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs +++ b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs @@ -16,6 +16,11 @@ /// public string Culture { get; set; } + /// + /// When dealing with content variants, this is the segment for the variant + /// + public string Segment { get; set; } + /// /// An array of metadata that is parsed out from the file info posted to the server which is set on the client. /// diff --git a/src/Umbraco.Core/Models/RelationTypeExtensions.cs b/src/Umbraco.Core/Models/RelationTypeExtensions.cs new file mode 100644 index 0000000000..4d9d6856cb --- /dev/null +++ b/src/Umbraco.Core/Models/RelationTypeExtensions.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Core.Models +{ + public static class RelationTypeExtensions + { + public static bool IsSystemRelationType(this IRelationType relationType) => + relationType.Alias == Constants.Conventions.RelationTypes.RelatedDocumentAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelatedMediaAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; + } +} diff --git a/src/Umbraco.Core/Models/Trees/MenuItem.cs b/src/Umbraco.Core/Models/Trees/MenuItem.cs index 9d4c76eea1..094c6b24ff 100644 --- a/src/Umbraco.Core/Models/Trees/MenuItem.cs +++ b/src/Umbraco.Core/Models/Trees/MenuItem.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Services; using Umbraco.Web.Actions; +using System.Threading; namespace Umbraco.Web.Models.Trees { @@ -28,12 +29,15 @@ namespace Umbraco.Web.Models.Trees Name = name; } - public MenuItem(string alias, ILocalizedTextService textService) : this() { + var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture); + values.TryGetValue($"visuallyHiddenTexts/{alias}_description", out var textDescription); + Alias = alias; Name = textService.Localize($"actions/{Alias}"); + TextDescription = textDescription; } /// @@ -74,6 +78,9 @@ namespace Umbraco.Web.Models.Trees [Required] public string Alias { get; set; } + [DataMember(Name = "textDescription")] + public string TextDescription { get; set; } + /// /// Ensures a menu separator will exist before this menu item /// diff --git a/src/Umbraco.Core/Trees/MenuItemList.cs b/src/Umbraco.Core/Trees/MenuItemList.cs index 546fa0390f..1410575fdb 100644 --- a/src/Umbraco.Core/Trees/MenuItemList.cs +++ b/src/Umbraco.Core/Trees/MenuItemList.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using Umbraco.Core.Services; using Umbraco.Web.Actions; @@ -50,14 +51,17 @@ namespace Umbraco.Web.Models.Trees var item = _actionCollection.GetAction(); if (item == null) return null; + var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture); + values.TryGetValue($"visuallyHiddenTexts/{item.Alias}Description", out var textDescription); + var menuItem = new MenuItem(item, textService.Localize($"actions/{item.Alias}")) { SeparatorBefore = hasSeparator, - OpensDialog = opensDialog + OpensDialog = opensDialog, + TextDescription = textDescription, }; return menuItem; } - } } diff --git a/src/Umbraco.Infrastructure/Models/ContentEditing/RelationTypeDisplay.cs b/src/Umbraco.Infrastructure/Models/ContentEditing/RelationTypeDisplay.cs index 49b0e15e6b..1d31f8a0de 100644 --- a/src/Umbraco.Infrastructure/Models/ContentEditing/RelationTypeDisplay.cs +++ b/src/Umbraco.Infrastructure/Models/ContentEditing/RelationTypeDisplay.cs @@ -13,6 +13,9 @@ namespace Umbraco.Web.Models.ContentEditing Notifications = new List(); } + [DataMember(Name = "isSystemRelationType")] + public bool IsSystemRelationType { get; set; } + /// /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) /// diff --git a/src/Umbraco.Infrastructure/Models/LocalPackageInstallModel.cs b/src/Umbraco.Infrastructure/Models/LocalPackageInstallModel.cs index 4eab8ed2c2..97d27ffdcb 100644 --- a/src/Umbraco.Infrastructure/Models/LocalPackageInstallModel.cs +++ b/src/Umbraco.Infrastructure/Models/LocalPackageInstallModel.cs @@ -9,10 +9,8 @@ namespace Umbraco.Web.Models /// A model that represents uploading a local package /// [DataContract(Name = "localPackageInstallModel")] - public class LocalPackageInstallModel : PackageInstallModel, IHaveUploadedFiles, INotificationModel + public class LocalPackageInstallModel : PackageInstallModel, INotificationModel { - public List UploadedFiles { get; } = new List(); - [DataMember(Name = "notifications")] public List Notifications { get; } = new List(); diff --git a/src/Umbraco.Infrastructure/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Infrastructure/Models/Mapping/RelationMapDefinition.cs index d6ec4fd969..836b04ca69 100644 --- a/src/Umbraco.Infrastructure/Models/Mapping/RelationMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Models/Mapping/RelationMapDefinition.cs @@ -39,6 +39,8 @@ namespace Umbraco.Web.Models.Mapping target.Udi = Udi.Create(Constants.UdiEntityType.RelationType, source.Key); target.Path = "-1," + source.Id; + target.IsSystemRelationType = source.IsSystemRelationType(); + // Set the "friendly" and entity names for the parent and child object types if (source.ParentObjectType.HasValue) { diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs index a52fc652bb..3f931d6358 100644 --- a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs @@ -3,9 +3,12 @@ using System.Data; using System.Data.SqlClient; using System.Diagnostics; using System.Linq; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Umbraco.Core.Configuration; +using System.Web; +using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -17,9 +20,10 @@ namespace Umbraco.Core.Runtime public class SqlMainDomLock : IMainDomLock { private string _lockId; - private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom"; + private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom"; private const string UpdatedSuffix = "_updated"; private readonly ILogger _logger; + private readonly IHostingEnvironment _hostingEnvironment; private IUmbracoDatabase _db; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); @@ -28,17 +32,20 @@ namespace Umbraco.Core.Runtime private bool _hasError; private object _locker = new object(); - public SqlMainDomLock(ILogger logger, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, IDbProviderFactoryCreator dbProviderFactoryCreator) + public SqlMainDomLock(ILogger logger, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, IDbProviderFactoryCreator dbProviderFactoryCreator, IHostingEnvironment hostingEnvironment) { // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer _lockId = Guid.NewGuid().ToString(); _logger = logger; + _hostingEnvironment = hostingEnvironment; _dbFactory = new UmbracoDatabaseFactory(_logger, globalSettings, connectionStrings, Constants.System.UmbracoConnectionName, new Lazy(() => new MapperCollection(Enumerable.Empty())), dbProviderFactoryCreator); + + MainDomKey = MainDomKeyPrefix + "-" + (NetworkHelper.MachineName + MainDom.GetMainDomId(_hostingEnvironment)).GenerateHash(); } public async Task AcquireLockAsync(int millisecondsTimeout) @@ -128,6 +135,16 @@ namespace Umbraco.Core.Runtime } + /// + /// Returns the keyvalue table key for the current server/app + /// + /// + /// The key is the the normal MainDomId which takes into account the AppDomainAppId and the physical file path of the app and this is + /// combined with the current machine name. The machine name is required because the default semaphore lock is machine wide so it implicitly + /// takes into account machine name whereas this needs to be explicitly per machine. + /// + private string MainDomKey { get; } + private void ListeningLoop() { while (true) diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index e5f3ffaa6a..815fe0a168 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -900,11 +900,12 @@ namespace Umbraco.Web.PublishedCache.NuCache // we ran this on a background thread then those cache refreshers are going to not get 'live' data when they query the content cache which // they require. - // These cannot currently be run side by side in parallel, due to the monitors need to be exits my the same thread that enter them. + // These can be run side by side in parallel. using (_contentStore.GetScopedWriteLock(_scopeProvider)) { NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _, out _); } + using (_mediaStore.GetScopedWriteLock(_scopeProvider)) { NotifyLocked(new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress.json b/src/Umbraco.Tests.AcceptanceTest/cypress.json index 051bf4a871..33978211ed 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress.json +++ b/src/Umbraco.Tests.AcceptanceTest/cypress.json @@ -6,5 +6,6 @@ "username": "", "password": "" }, - "supportFile": "cypress/support/index.ts" + "supportFile": "cypress/support/index.ts", + "videoUploadOnPasses" : false } diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts index 17b7bb6805..49bcf94943 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts @@ -6,7 +6,7 @@ context('Languages', () => { }); it('Add language', () => { - const name = "Neddersass’sch (Nedderlannen)"; // Must be an option in the select box + const name = "Kyrgyz (Kyrgyzstan)"; // Must be an option in the select box cy.umbracoEnsureLanguageNameNotExists(name); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts index bbabbcb4bf..6871db7ffe 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts @@ -1,4 +1,6 @@ /// +import {DocumentTypeBuilder, TemplateBuilder} from "umbraco-cypress-testhelpers"; + context('Templates', () => { beforeEach(() => { @@ -6,28 +8,50 @@ context('Templates', () => { }); it('Create template', () => { - const name = "Test template"; + const name = "Test template"; - cy.umbracoEnsureTemplateNameNotExists(name); + cy.umbracoEnsureTemplateNameNotExists(name); - cy.umbracoSection('settings'); - cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); - cy.umbracoTreeItem("settings", ["Templates"]).rightclick(); + cy.umbracoTreeItem("settings", ["Templates"]).rightclick(); - cy.umbracoContextMenuAction("action-create").click(); + cy.umbracoContextMenuAction("action-create").click(); - //Type name - cy.umbracoEditorHeaderName(name); + //Type name + cy.umbracoEditorHeaderName(name); - //Save - cy.get('.btn-success').click(); + //Save + cy.get("form[name='contentForm']").submit(); - //Assert - cy.umbracoSuccessNotification().should('be.visible'); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); - //Clean up - cy.umbracoEnsureTemplateNameNotExists(name); + //Clean up + cy.umbracoEnsureTemplateNameNotExists(name); }); + it('Delete template', () => { + const name = "Test template"; + cy.umbracoEnsureTemplateNameNotExists(name); + + const template = new TemplateBuilder() + .withName(name) + .build(); + + cy.saveTemplate(template); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Templates", name]).rightclick(); + cy.umbracoContextMenuAction("action-delete").click(); + + cy.umbracoButtonByLabelKey("general_ok").click(); + + cy.contains(name).should('not.exist'); + + cy.umbracoEnsureTemplateNameNotExists(name); + }); }); diff --git a/src/Umbraco.Tests.AcceptanceTest/package.json b/src/Umbraco.Tests.AcceptanceTest/package.json index daa1c424bb..ad125d090a 100644 --- a/src/Umbraco.Tests.AcceptanceTest/package.json +++ b/src/Umbraco.Tests.AcceptanceTest/package.json @@ -6,8 +6,8 @@ "devDependencies": { "cross-env": "^7.0.2", "ncp": "^2.0.0", - "cypress": "^4.5.0", - "umbraco-cypress-testhelpers": "1.0.0-beta-38" + "cypress": "^4.6.0", + "umbraco-cypress-testhelpers": "1.0.0-beta-39" }, "dependencies": { "typescript": "^3.9.2" diff --git a/src/Umbraco.Tests/Composing/ContainerConformingTests.cs b/src/Umbraco.Tests/Composing/ContainerConformingTests.cs index 4ec6dfc0d5..6819823dd5 100644 --- a/src/Umbraco.Tests/Composing/ContainerConformingTests.cs +++ b/src/Umbraco.Tests/Composing/ContainerConformingTests.cs @@ -333,6 +333,24 @@ namespace Umbraco.Tests.Composing Assert.AreSame(s1, s2); } + [Test] + public void CanRegisterMultipleSameTypeParametersWithCreateInstance() + { + var register = GetRegister(); + + register.Register(c => + { + const string param1 = "param1"; + const string param2 = "param2"; + + return c.CreateInstance(param1, param2); + }); + + var factory = register.CreateFactory(); + var instance = factory.GetInstance(); + Assert.AreNotEqual(instance.Thing, instance.AnotherThing); + } + public interface IThing { } public abstract class ThingBase : IThing { } @@ -353,5 +371,17 @@ namespace Umbraco.Tests.Composing public IEnumerable Things { get; } } + + public class Thing4 : ThingBase + { + public readonly string Thing; + public readonly string AnotherThing; + + public Thing4(string thing, string anotherThing) + { + Thing = thing; + AnotherThing = anotherThing; + } + } } } diff --git a/src/Umbraco.Web/Editors/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs similarity index 66% rename from src/Umbraco.Web/Editors/CodeFileController.cs rename to src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index 2de3283b95..2d0ffc5d33 100644 --- a/src/Umbraco.Web/Editors/CodeFileController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs @@ -1,59 +1,66 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; -using System.Web.Http; +using Microsoft.AspNetCore.Mvc; using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; + using Umbraco.Core.Configuration; + using Umbraco.Core.Configuration.Legacy; using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Persistence; + using Umbraco.Core.Mapping; + using Umbraco.Core.Models; using Umbraco.Core.Services; -using Umbraco.Core.Strings; -using Umbraco.Core.Strings.Css; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; -using Umbraco.Web.Trees; + using Umbraco.Core.Strings; + using Umbraco.Core.Strings.Css; + using Umbraco.Extensions; + using Umbraco.Web.Models.ContentEditing; using Stylesheet = Umbraco.Core.Models.Stylesheet; using StylesheetRule = Umbraco.Web.Models.ContentEditing.StylesheetRule; -using Umbraco.Core.Mapping; -using Umbraco.Web.Routing; +using Umbraco.Web.BackOffice.Filters; + using Umbraco.Web.Common.ActionsResults; + using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Editors; -namespace Umbraco.Web.Editors + namespace Umbraco.Web.BackOffice.Controllers { // TODO: Put some exception filters in our webapi to return 404 instead of 500 when we throw ArgumentNullException // ref: https://www.exceptionnotfound.net/the-asp-net-web-api-exception-handling-pipeline-a-guided-tour/ [PluginController("UmbracoApi")] - [PrefixlessBodyModelValidator] - [UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)] + //[PrefixlessBodyModelValidator] + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Settings)] public class CodeFileController : BackOfficeNotificationsController { private readonly IIOHelper _ioHelper; private readonly IFileSystems _fileSystems; + private readonly IFileService _fileService; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly ILocalizedTextService _localizedTextService; + private readonly UmbracoMapper _umbracoMapper; + private readonly IShortStringHelper _shortStringHelper; + private readonly IGlobalSettings _globalSettings; public CodeFileController( - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger logger, - IRuntimeState runtimeState, - IShortStringHelper shortStringHelper, - UmbracoMapper umbracoMapper, IIOHelper ioHelper, IFileSystems fileSystems, - IPublishedUrlProvider publishedUrlProvider) - : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) + IFileService fileService, + IUmbracoContextAccessor umbracoContextAccessor, + ILocalizedTextService localizedTextService, + UmbracoMapper umbracoMapper, + IShortStringHelper shortStringHelper, + IGlobalSettings globalSettings) { + _ioHelper = ioHelper; _fileSystems = fileSystems; + _fileService = fileService; + _umbracoContextAccessor = umbracoContextAccessor; + _localizedTextService = localizedTextService; + _umbracoMapper = umbracoMapper; + _shortStringHelper = shortStringHelper; + _globalSettings = globalSettings; } /// @@ -63,32 +70,33 @@ namespace Umbraco.Web.Editors /// /// Will return a simple 200 if file creation succeeds [ValidationFilter] - public HttpResponseMessage PostCreate(string type, CodeFileDisplay display) + public ActionResult PostCreate(string type, CodeFileDisplay display) { if (display == null) throw new ArgumentNullException("display"); if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); + var currentUser = _umbracoContextAccessor.GetRequiredUmbracoContext().Security.CurrentUser; switch (type) { case Core.Constants.Trees.PartialViews: var view = new PartialView(PartialViewType.PartialView, display.VirtualPath); view.Content = display.Content; - var result = Services.FileService.CreatePartialView(view, display.Snippet, Security.CurrentUser.Id); - return result.Success == true ? Request.CreateResponse(HttpStatusCode.OK) : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); + var result = _fileService.CreatePartialView(view, display.Snippet, currentUser.Id); + return result.Success == true ? Ok() : throw HttpResponseException.CreateNotificationValidationErrorResponse(result.Exception.Message); case Core.Constants.Trees.PartialViewMacros: var viewMacro = new PartialView(PartialViewType.PartialViewMacro, display.VirtualPath); viewMacro.Content = display.Content; - var resultMacro = Services.FileService.CreatePartialViewMacro(viewMacro, display.Snippet, Security.CurrentUser.Id); - return resultMacro.Success == true ? Request.CreateResponse(HttpStatusCode.OK) : Request.CreateNotificationValidationErrorResponse(resultMacro.Exception.Message); + var resultMacro = _fileService.CreatePartialViewMacro(viewMacro, display.Snippet, currentUser.Id); + return resultMacro.Success == true ? Ok() : throw HttpResponseException.CreateNotificationValidationErrorResponse(resultMacro.Exception.Message); case Core.Constants.Trees.Scripts: var script = new Script(display.VirtualPath); - Services.FileService.SaveScript(script, Security.CurrentUser.Id); - return Request.CreateResponse(HttpStatusCode.OK); + _fileService.SaveScript(script, currentUser.Id); + return Ok(); default: - return Request.CreateResponse(HttpStatusCode.NotFound); + return NotFound(); } } @@ -100,13 +108,13 @@ namespace Umbraco.Web.Editors /// The name of the container/folder /// [HttpPost] - public HttpResponseMessage PostCreateContainer(string type, string parentId, string name) + public ActionResult PostCreateContainer(string type, string parentId, string name) { if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); if (string.IsNullOrWhiteSpace(parentId)) throw new ArgumentException("Value cannot be null or whitespace.", "parentId"); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); if (name.ContainsAny(Path.GetInvalidPathChars())) { - return Request.CreateNotificationValidationErrorResponse(Services.TextService.Localize("codefile/createFolderIllegalChars")); + throw HttpResponseException.CreateNotificationValidationErrorResponse(_localizedTextService.Localize("codefile/createFolderIllegalChars")); } // if the parentId is root (-1) then we just need an empty string as we are @@ -129,28 +137,28 @@ namespace Umbraco.Web.Editors { case Core.Constants.Trees.PartialViews: virtualPath = NormalizeVirtualPath(name, Core.Constants.SystemDirectories.PartialViews); - Services.FileService.CreatePartialViewFolder(virtualPath); + _fileService.CreatePartialViewFolder(virtualPath); break; case Core.Constants.Trees.PartialViewMacros: virtualPath = NormalizeVirtualPath(name, Core.Constants.SystemDirectories.MacroPartials); - Services.FileService.CreatePartialViewMacroFolder(virtualPath); + _fileService.CreatePartialViewMacroFolder(virtualPath); break; case Core.Constants.Trees.Scripts: - virtualPath = NormalizeVirtualPath(name, GlobalSettings.UmbracoScriptsPath); - Services.FileService.CreateScriptFolder(virtualPath); + virtualPath = NormalizeVirtualPath(name, _globalSettings.UmbracoScriptsPath); + _fileService.CreateScriptFolder(virtualPath); break; case Core.Constants.Trees.Stylesheets: - virtualPath = NormalizeVirtualPath(name, GlobalSettings.UmbracoCssPath); - Services.FileService.CreateStyleSheetFolder(virtualPath); + virtualPath = NormalizeVirtualPath(name, _globalSettings.UmbracoCssPath); + _fileService.CreateStyleSheetFolder(virtualPath); break; } - return Request.CreateResponse(HttpStatusCode.OK, new CodeFileDisplay + return new CodeFileDisplay { VirtualPath = virtualPath, Path = Url.GetTreePathFromFilePath(virtualPath) - }); + }; } /// @@ -159,7 +167,7 @@ namespace Umbraco.Web.Editors /// This is a string but will be 'scripts' 'partialViews', 'partialViewMacros' or 'stylesheets' /// The filename or urlencoded path of the file to open /// The file and its contents from the virtualPath - public CodeFileDisplay GetByPath(string type, string virtualPath) + public ActionResult GetByPath(string type, string virtualPath) { if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); if (string.IsNullOrWhiteSpace(virtualPath)) throw new ArgumentException("Value cannot be null or whitespace.", "virtualPath"); @@ -168,56 +176,54 @@ namespace Umbraco.Web.Editors switch (type) { - case Core.Constants.Trees.PartialViews: - var view = Services.FileService.GetPartialView(virtualPath); + case Constants.Trees.PartialViews: + var view = _fileService.GetPartialView(virtualPath); if (view != null) { - var display = Mapper.Map(view); - display.FileType = Core.Constants.Trees.PartialViews; + var display = _umbracoMapper.Map(view); + display.FileType = Constants.Trees.PartialViews; display.Path = Url.GetTreePathFromFilePath(view.Path); display.Id = System.Web.HttpUtility.UrlEncode(view.Path); return display; } - throw new HttpResponseException(HttpStatusCode.NotFound); - case Core.Constants.Trees.PartialViewMacros: - var viewMacro = Services.FileService.GetPartialViewMacro(virtualPath); + break; + case Constants.Trees.PartialViewMacros: + var viewMacro = _fileService.GetPartialViewMacro(virtualPath); if (viewMacro != null) { - var display = Mapper.Map(viewMacro); - display.FileType = Core.Constants.Trees.PartialViewMacros; + var display = _umbracoMapper.Map(viewMacro); + display.FileType = Constants.Trees.PartialViewMacros; display.Path = Url.GetTreePathFromFilePath(viewMacro.Path); display.Id = System.Web.HttpUtility.UrlEncode(viewMacro.Path); return display; } - throw new HttpResponseException(HttpStatusCode.NotFound); - - case Core.Constants.Trees.Scripts: - var script = Services.FileService.GetScriptByName(virtualPath); + break; + case Constants.Trees.Scripts: + var script = _fileService.GetScriptByName(virtualPath); if (script != null) { - var display = Mapper.Map(script); - display.FileType = Core.Constants.Trees.Scripts; + var display = _umbracoMapper.Map(script); + display.FileType = Constants.Trees.Scripts; display.Path = Url.GetTreePathFromFilePath(script.Path); display.Id = System.Web.HttpUtility.UrlEncode(script.Path); return display; } - throw new HttpResponseException(HttpStatusCode.NotFound); - - case Core.Constants.Trees.Stylesheets: - var stylesheet = Services.FileService.GetStylesheetByName(virtualPath); + break; + case Constants.Trees.Stylesheets: + var stylesheet = _fileService.GetStylesheetByName(virtualPath); if (stylesheet != null) { - var display = Mapper.Map(stylesheet); - display.FileType = Core.Constants.Trees.Stylesheets; + var display = _umbracoMapper.Map(stylesheet); + display.FileType = Constants.Trees.Stylesheets; display.Path = Url.GetTreePathFromFilePath(stylesheet.Path); display.Id = System.Web.HttpUtility.UrlEncode(stylesheet.Path); return display; } - throw new HttpResponseException(HttpStatusCode.NotFound); + break; } - throw new HttpResponseException(HttpStatusCode.NotFound); + return NotFound(); } /// @@ -233,7 +239,7 @@ namespace Umbraco.Web.Editors switch (type) { case Core.Constants.Trees.PartialViews: - snippets = Services.FileService.GetPartialViewSnippetNames( + snippets = _fileService.GetPartialViewSnippetNames( //ignore these - (this is taken from the logic in "PartialView.ascx.cs") "Gallery", "ListChildPagesFromChangeableSource", @@ -241,13 +247,13 @@ namespace Umbraco.Web.Editors "ListImagesFromMediaFolder"); break; case Core.Constants.Trees.PartialViewMacros: - snippets = Services.FileService.GetPartialViewSnippetNames(); + snippets = _fileService.GetPartialViewSnippetNames(); break; default: throw new HttpResponseException(HttpStatusCode.NotFound); } - return snippets.Select(snippet => new SnippetDisplay() {Name = snippet.SplitPascalCasing(ShortStringHelper).ToFirstUpperInvariant(), FileName = snippet}); + return snippets.Select(snippet => new SnippetDisplay() {Name = snippet.SplitPascalCasing(_shortStringHelper).ToFirstUpperInvariant(), FileName = snippet}); } /// @@ -257,37 +263,37 @@ namespace Umbraco.Web.Editors /// /// /// - public CodeFileDisplay GetScaffold(string type, string id, string snippetName = null) + public ActionResult GetScaffold(string type, string id, string snippetName = null) { if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Value cannot be null or whitespace.", "id"); - CodeFileDisplay codeFileDisplay; + CodeFileDisplay codeFileDisplay; switch (type) { case Core.Constants.Trees.PartialViews: - codeFileDisplay = Mapper.Map(new PartialView(PartialViewType.PartialView, string.Empty)); + codeFileDisplay = _umbracoMapper.Map(new PartialView(PartialViewType.PartialView, string.Empty)); codeFileDisplay.VirtualPath = Core.Constants.SystemDirectories.PartialViews; if (snippetName.IsNullOrWhiteSpace() == false) - codeFileDisplay.Content = Services.FileService.GetPartialViewSnippetContent(snippetName); + codeFileDisplay.Content = _fileService.GetPartialViewSnippetContent(snippetName); break; case Core.Constants.Trees.PartialViewMacros: - codeFileDisplay = Mapper.Map(new PartialView(PartialViewType.PartialViewMacro, string.Empty)); + codeFileDisplay = _umbracoMapper.Map(new PartialView(PartialViewType.PartialViewMacro, string.Empty)); codeFileDisplay.VirtualPath = Core.Constants.SystemDirectories.MacroPartials; if (snippetName.IsNullOrWhiteSpace() == false) - codeFileDisplay.Content = Services.FileService.GetPartialViewMacroSnippetContent(snippetName); + codeFileDisplay.Content = _fileService.GetPartialViewMacroSnippetContent(snippetName); break; case Core.Constants.Trees.Scripts: - codeFileDisplay = Mapper.Map(new Script(string.Empty)); - codeFileDisplay.VirtualPath = GlobalSettings.UmbracoScriptsPath; + codeFileDisplay = _umbracoMapper.Map(new Script(string.Empty)); + codeFileDisplay.VirtualPath = _globalSettings.UmbracoScriptsPath; break; case Core.Constants.Trees.Stylesheets: - codeFileDisplay = Mapper.Map(new Stylesheet(string.Empty)); - codeFileDisplay.VirtualPath = GlobalSettings.UmbracoCssPath; + codeFileDisplay = _umbracoMapper.Map(new Stylesheet(string.Empty)); + codeFileDisplay.VirtualPath = _globalSettings.UmbracoCssPath; break; default: - throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Unsupported editortype")); + return new UmbracoProblemResult("Unsupported editortype", HttpStatusCode.BadRequest); } // Make sure that the root virtual path ends with '/' @@ -313,70 +319,66 @@ namespace Umbraco.Web.Editors /// Will return a simple 200 if file deletion succeeds [HttpDelete] [HttpPost] - public HttpResponseMessage Delete(string type, string virtualPath) + public IActionResult Delete(string type, string virtualPath) { if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); if (string.IsNullOrWhiteSpace(virtualPath)) throw new ArgumentException("Value cannot be null or whitespace.", "virtualPath"); virtualPath = System.Web.HttpUtility.UrlDecode(virtualPath); - + var currentUser = _umbracoContextAccessor.GetRequiredUmbracoContext().Security.CurrentUser; switch (type) { - case Core.Constants.Trees.PartialViews: - if (IsDirectory(virtualPath, Core.Constants.SystemDirectories.PartialViews)) + case Constants.Trees.PartialViews: + if (IsDirectory(virtualPath, Constants.SystemDirectories.PartialViews)) { - Services.FileService.DeletePartialViewFolder(virtualPath); - return Request.CreateResponse(HttpStatusCode.OK); + _fileService.DeletePartialViewFolder(virtualPath); + return Ok(); } - if (Services.FileService.DeletePartialView(virtualPath, Security.CurrentUser.Id)) + if (_fileService.DeletePartialView(virtualPath, currentUser.Id)) { - return Request.CreateResponse(HttpStatusCode.OK); + return Ok(); } - return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Partial View or folder found with the specified path"); + return new UmbracoProblemResult("No Partial View or folder found with the specified path", HttpStatusCode.NotFound); - case Core.Constants.Trees.PartialViewMacros: - if (IsDirectory(virtualPath, Core.Constants.SystemDirectories.MacroPartials)) + case Constants.Trees.PartialViewMacros: + if (IsDirectory(virtualPath, Constants.SystemDirectories.MacroPartials)) { - Services.FileService.DeletePartialViewMacroFolder(virtualPath); - return Request.CreateResponse(HttpStatusCode.OK); + _fileService.DeletePartialViewMacroFolder(virtualPath); + return Ok(); } - if (Services.FileService.DeletePartialViewMacro(virtualPath, Security.CurrentUser.Id)) + if (_fileService.DeletePartialViewMacro(virtualPath, currentUser.Id)) { - return Request.CreateResponse(HttpStatusCode.OK); + return Ok(); } - return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Partial View Macro or folder found with the specified path"); + return new UmbracoProblemResult("No Partial View Macro or folder found with the specified path", HttpStatusCode.NotFound); - case Core.Constants.Trees.Scripts: - if (IsDirectory(virtualPath, GlobalSettings.UmbracoScriptsPath)) + case Constants.Trees.Scripts: + if (IsDirectory(virtualPath, _globalSettings.UmbracoScriptsPath)) { - Services.FileService.DeleteScriptFolder(virtualPath); - return Request.CreateResponse(HttpStatusCode.OK); + _fileService.DeleteScriptFolder(virtualPath); + return Ok(); } - if (Services.FileService.GetScriptByName(virtualPath) != null) + if (_fileService.GetScriptByName(virtualPath) != null) { - Services.FileService.DeleteScript(virtualPath, Security.CurrentUser.Id); - return Request.CreateResponse(HttpStatusCode.OK); + _fileService.DeleteScript(virtualPath, currentUser.Id); + return Ok(); } - return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Script or folder found with the specified path"); - - case Core.Constants.Trees.Stylesheets: - if (IsDirectory(virtualPath, GlobalSettings.UmbracoCssPath)) + return new UmbracoProblemResult("No Script or folder found with the specified path", HttpStatusCode.NotFound); + case Constants.Trees.Stylesheets: + if (IsDirectory(virtualPath, _globalSettings.UmbracoCssPath)) { - Services.FileService.DeleteStyleSheetFolder(virtualPath); - return Request.CreateResponse(HttpStatusCode.OK); + _fileService.DeleteStyleSheetFolder(virtualPath); + return Ok(); } - if (Services.FileService.GetStylesheetByName(virtualPath) != null) + if (_fileService.GetStylesheetByName(virtualPath) != null) { - Services.FileService.DeleteStylesheet(virtualPath, Security.CurrentUser.Id); - return Request.CreateResponse(HttpStatusCode.OK); + _fileService.DeleteStylesheet(virtualPath, currentUser.Id); + return Ok(); } - return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Stylesheet found with the specified path"); - + return new UmbracoProblemResult("No Stylesheet found with the specified path", HttpStatusCode.NotFound); default: - return Request.CreateResponse(HttpStatusCode.NotFound); + return NotFound(); } - - throw new HttpResponseException(HttpStatusCode.NotFound); } /// @@ -384,69 +386,70 @@ namespace Umbraco.Web.Editors /// /// /// The updated CodeFileDisplay model - public CodeFileDisplay PostSave(CodeFileDisplay display) + public ActionResult PostSave(CodeFileDisplay display) { if (display == null) throw new ArgumentNullException("display"); + TryValidateModel(display); if (ModelState.IsValid == false) { - throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + return ValidationProblem(ModelState); } switch (display.FileType) { - case Core.Constants.Trees.PartialViews: + case Constants.Trees.PartialViews: var partialViewResult = CreateOrUpdatePartialView(display); if (partialViewResult.Success) { - display = Mapper.Map(partialViewResult.Result, display); + display = _umbracoMapper.Map(partialViewResult.Result, display); display.Path = Url.GetTreePathFromFilePath(partialViewResult.Result.Path); display.Id = System.Web.HttpUtility.UrlEncode(partialViewResult.Result.Path); return display; } display.AddErrorNotification( - Services.TextService.Localize("speechBubbles/partialViewErrorHeader"), - Services.TextService.Localize("speechBubbles/partialViewErrorText")); + _localizedTextService.Localize("speechBubbles/partialViewErrorHeader"), + _localizedTextService.Localize("speechBubbles/partialViewErrorText")); break; - case Core.Constants.Trees.PartialViewMacros: + case Constants.Trees.PartialViewMacros: var partialViewMacroResult = CreateOrUpdatePartialViewMacro(display); if (partialViewMacroResult.Success) { - display = Mapper.Map(partialViewMacroResult.Result, display); + display = _umbracoMapper.Map(partialViewMacroResult.Result, display); display.Path = Url.GetTreePathFromFilePath(partialViewMacroResult.Result.Path); display.Id = System.Web.HttpUtility.UrlEncode(partialViewMacroResult.Result.Path); return display; } display.AddErrorNotification( - Services.TextService.Localize("speechBubbles/partialViewErrorHeader"), - Services.TextService.Localize("speechBubbles/partialViewErrorText")); + _localizedTextService.Localize("speechBubbles/partialViewErrorHeader"), + _localizedTextService.Localize("speechBubbles/partialViewErrorText")); break; - case Core.Constants.Trees.Scripts: + case Constants.Trees.Scripts: var scriptResult = CreateOrUpdateScript(display); - display = Mapper.Map(scriptResult, display); + display = _umbracoMapper.Map(scriptResult, display); display.Path = Url.GetTreePathFromFilePath(scriptResult.Path); display.Id = System.Web.HttpUtility.UrlEncode(scriptResult.Path); return display; //display.AddErrorNotification( - // Services.TextService.Localize("speechBubbles/partialViewErrorHeader"), - // Services.TextService.Localize("speechBubbles/partialViewErrorText")); + // _localizedTextService.Localize("speechBubbles/partialViewErrorHeader"), + // _localizedTextService.Localize("speechBubbles/partialViewErrorText")); - case Core.Constants.Trees.Stylesheets: + case Constants.Trees.Stylesheets: var stylesheetResult = CreateOrUpdateStylesheet(display); - display = Mapper.Map(stylesheetResult, display); + display = _umbracoMapper.Map(stylesheetResult, display); display.Path = Url.GetTreePathFromFilePath(stylesheetResult.Path); display.Id = System.Web.HttpUtility.UrlEncode(stylesheetResult.Path); return display; default: - throw new HttpResponseException(HttpStatusCode.NotFound); + return NotFound(); } return display; @@ -524,16 +527,16 @@ namespace Umbraco.Web.Editors private IScript CreateOrUpdateScript(CodeFileDisplay display) { return CreateOrUpdateFile(display, ".js", _fileSystems.ScriptsFileSystem, - name => Services.FileService.GetScriptByName(name), - (script, userId) => Services.FileService.SaveScript(script, userId), + name => _fileService.GetScriptByName(name), + (script, userId) => _fileService.SaveScript(script, userId), name => new Script(name)); } private IStylesheet CreateOrUpdateStylesheet(CodeFileDisplay display) { return CreateOrUpdateFile(display, ".css", _fileSystems.StylesheetsFileSystem, - name => Services.FileService.GetStylesheetByName(name), - (stylesheet, userId) => Services.FileService.SaveStylesheet(stylesheet, userId), + name => _fileService.GetStylesheetByName(name), + (stylesheet, userId) => _fileService.SaveStylesheet(stylesheet, userId), name => new Stylesheet(name) ); } @@ -555,7 +558,7 @@ namespace Umbraco.Web.Editors ? relPath + display.Name : relPath.EnsureEndsWith('/') + display.Name; } - + var currentUser = _umbracoContextAccessor.GetRequiredUmbracoContext().Security.CurrentUser; var file = getFileByName(relPath); if (file != null) { @@ -565,13 +568,13 @@ namespace Umbraco.Web.Editors file.Content = display.Content; //try/catch? since this doesn't return an Attempt? - saveFile(file, Security.CurrentUser.Id); + saveFile(file, currentUser.Id); } else { file = createFile(relPath); file.Content = display.Content; - saveFile(file, Security.CurrentUser.Id); + saveFile(file, currentUser.Id); } return file; @@ -580,13 +583,13 @@ namespace Umbraco.Web.Editors private Attempt CreateOrUpdatePartialView(CodeFileDisplay display) { return CreateOrUpdatePartialView(display, Core.Constants.SystemDirectories.PartialViews, - Services.FileService.GetPartialView, Services.FileService.SavePartialView, Services.FileService.CreatePartialView); + _fileService.GetPartialView, _fileService.SavePartialView, _fileService.CreatePartialView); } private Attempt CreateOrUpdatePartialViewMacro(CodeFileDisplay display) { return CreateOrUpdatePartialView(display, Core.Constants.SystemDirectories.MacroPartials, - Services.FileService.GetPartialViewMacro, Services.FileService.SavePartialViewMacro, Services.FileService.CreatePartialViewMacro); + _fileService.GetPartialViewMacro, _fileService.SavePartialViewMacro, _fileService.CreatePartialViewMacro); } /// @@ -608,6 +611,8 @@ namespace Umbraco.Web.Editors display.Name = EnsureCorrectFileExtension(display.Name, ".cshtml"); Attempt partialViewResult; + var currentUser = _umbracoContextAccessor.GetRequiredUmbracoContext().Security.CurrentUser; + var virtualPath = NormalizeVirtualPath(display.VirtualPath, systemDirectory); var view = getView(virtualPath); if (view != null) @@ -617,13 +622,13 @@ namespace Umbraco.Web.Editors view.Path = orgPath + display.Name; view.Content = display.Content; - partialViewResult = saveView(view, Security.CurrentUser.Id); + partialViewResult = saveView(view, currentUser.Id); } else { view = new PartialView(PartialViewType.PartialView, virtualPath + display.Name); view.Content = display.Content; - partialViewResult = createView(view, display.Snippet, Security.CurrentUser.Id); + partialViewResult = createView(view, display.Snippet, currentUser.Id); } return partialViewResult; diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs similarity index 63% rename from src/Umbraco.Web/Editors/DataTypeController.cs rename to src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs index 19ad546b2d..0d66a5e329 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs @@ -3,32 +3,23 @@ using System.Collections.Generic; using System.Data; using System.Linq; using System.Net; -using System.Web.Http; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; -using Umbraco.Core.Strings; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; -using System.Net.Http; using System.Text; -using Umbraco.Core.Cache; -using Umbraco.Web.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; using Constants = Umbraco.Core.Constants; using Umbraco.Core.Mapping; -using System.Web.Http.Controllers; +using Microsoft.AspNetCore.Mvc; using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Web.Routing; +using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Editors; -namespace Umbraco.Web.Editors +namespace Umbraco.Web.BackOffice.Controllers { - /// /// The API controller used for editing data types /// @@ -37,45 +28,43 @@ namespace Umbraco.Web.Editors /// Content Types, Member Types or Media Types ... and of course to Data Types /// [PluginController("UmbracoApi")] - [UmbracoTreeAuthorize(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)] - [EnableOverrideAuthorization] - [DataTypeControllerConfiguration] + [UmbracoTreeAuthorizeAttribute(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)] public class DataTypeController : BackOfficeNotificationsController { private readonly PropertyEditorCollection _propertyEditors; + private readonly IDataTypeService _dataTypeService; private readonly IContentSettings _contentSettings; + private readonly UmbracoMapper _umbracoMapper; + private readonly PropertyEditorCollection _propertyEditorCollection; + private readonly IContentTypeService _contentTypeService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IMemberTypeService _memberTypeService; + private readonly ILocalizedTextService _localizedTextService; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; public DataTypeController( PropertyEditorCollection propertyEditors, - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger logger, - IRuntimeState runtimeState, - IShortStringHelper shortStringHelper, - UmbracoMapper umbracoMapper, + IDataTypeService dataTypeService, IContentSettings contentSettings, - IPublishedUrlProvider publishedUrlProvider) - : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) - { - _propertyEditors = propertyEditors; + UmbracoMapper umbracoMapper, + PropertyEditorCollection propertyEditorCollection, + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + ILocalizedTextService localizedTextService, + IUmbracoContextAccessor umbracoContextAccessor) + { + _propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); + _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); _contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings)); - } - - /// - /// Configures this controller with a custom action selector - /// - private class DataTypeControllerConfigurationAttribute : Attribute, IControllerConfiguration - { - public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) - { - controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) - )); - } - } + _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + _propertyEditorCollection = propertyEditorCollection ?? throw new ArgumentNullException(nameof(propertyEditorCollection)); + _contentTypeService = contentTypeService ?? throw new ArgumentNullException(nameof(contentTypeService)); + _mediaTypeService = mediaTypeService ?? throw new ArgumentNullException(nameof(mediaTypeService)); + _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); + _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + } /// /// Gets data type by name @@ -84,8 +73,8 @@ namespace Umbraco.Web.Editors /// public DataTypeDisplay GetByName(string name) { - var dataType = Services.DataTypeService.GetDataType(name); - return dataType == null ? null : Mapper.Map(dataType); + var dataType = _dataTypeService.GetDataType(name); + return dataType == null ? null : _umbracoMapper.Map(dataType); } /// @@ -93,14 +82,15 @@ namespace Umbraco.Web.Editors /// /// /// + [DetermineAmbiguousActionByPassingParameters] public DataTypeDisplay GetById(int id) { - var dataType = Services.DataTypeService.GetDataType(id); + var dataType = _dataTypeService.GetDataType(id); if (dataType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } - return Mapper.Map(dataType); + return _umbracoMapper.Map(dataType); } /// @@ -108,14 +98,15 @@ namespace Umbraco.Web.Editors /// /// /// + [DetermineAmbiguousActionByPassingParameters] public DataTypeDisplay GetById(Guid id) { - var dataType = Services.DataTypeService.GetDataType(id); + var dataType = _dataTypeService.GetDataType(id); if (dataType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } - return Mapper.Map(dataType); + return _umbracoMapper.Map(dataType); } /// @@ -123,18 +114,19 @@ namespace Umbraco.Web.Editors /// /// /// + [DetermineAmbiguousActionByPassingParameters] public DataTypeDisplay GetById(Udi id) { var guidUdi = id as GuidUdi; if (guidUdi == null) throw new HttpResponseException(HttpStatusCode.NotFound); - var dataType = Services.DataTypeService.GetDataType(guidUdi.Guid); + var dataType = _dataTypeService.GetDataType(guidUdi.Guid); if (dataType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } - return Mapper.Map(dataType); + return _umbracoMapper.Map(dataType); } /// @@ -144,17 +136,17 @@ namespace Umbraco.Web.Editors /// [HttpDelete] [HttpPost] - public HttpResponseMessage DeleteById(int id) + public IActionResult DeleteById(int id) { - var foundType = Services.DataTypeService.GetDataType(id); + var foundType = _dataTypeService.GetDataType(id); if (foundType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } + var currentUser = _umbracoContextAccessor.GetRequiredUmbracoContext().Security.CurrentUser; + _dataTypeService.Delete(foundType, currentUser.Id); - Services.DataTypeService.Delete(foundType, Security.CurrentUser.Id); - - return Request.CreateResponse(HttpStatusCode.OK); + return Ok(); } public DataTypeDisplay GetEmpty(int parentId) @@ -162,7 +154,7 @@ namespace Umbraco.Web.Editors // cannot create an "empty" data type, so use something by default. var editor = _propertyEditors[Constants.PropertyEditors.Aliases.Label]; var dt = new DataType(editor, parentId); - return Mapper.Map(dt); + return _umbracoMapper.Map(dt); } /// @@ -172,13 +164,13 @@ namespace Umbraco.Web.Editors /// a DataTypeDisplay public DataTypeDisplay GetCustomListView(string contentTypeAlias) { - var dt = Services.DataTypeService.GetDataType(Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias); + var dt = _dataTypeService.GetDataType(Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias); if (dt == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } - return Mapper.Map(dt); + return _umbracoMapper.Map(dt); } /// @@ -188,17 +180,17 @@ namespace Umbraco.Web.Editors /// public DataTypeDisplay PostCreateCustomListView(string contentTypeAlias) { - var dt = Services.DataTypeService.GetDataType(Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias); + var dt = _dataTypeService.GetDataType(Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias); //if it doesn't exist yet, we will create it. if (dt == null) { var editor = _propertyEditors[Constants.PropertyEditors.Aliases.ListView]; dt = new DataType(editor) { Name = Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias }; - Services.DataTypeService.Save(dt); + _dataTypeService.Save(dt); } - return Mapper.Map(dt); + return _umbracoMapper.Map(dt); } /// @@ -218,11 +210,11 @@ namespace Umbraco.Web.Editors if (dataTypeId == -1) { //this is a new data type, so just return the field editors with default values - return Mapper.Map>(propEd); + return _umbracoMapper.Map>(propEd); } //we have a data type associated - var dataType = Services.DataTypeService.GetDataType(dataTypeId); + var dataType = _dataTypeService.GetDataType(dataTypeId); if (dataType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); @@ -234,11 +226,11 @@ namespace Umbraco.Web.Editors if (dataType.EditorAlias == editorAlias) { //this is the currently assigned pre-value editor, return with values. - return Mapper.Map>(dataType); + return _umbracoMapper.Map>(dataType); } //these are new pre-values, so just return the field editors with default values - return Mapper.Map>(propEd); + return _umbracoMapper.Map>(propEd); } /// @@ -248,20 +240,23 @@ namespace Umbraco.Web.Editors /// [HttpDelete] [HttpPost] - public HttpResponseMessage DeleteContainer(int id) + public IActionResult DeleteContainer(int id) { - Services.DataTypeService.DeleteContainer(id, Security.CurrentUser.Id); - return Request.CreateResponse(HttpStatusCode.OK); + var currentUser = _umbracoContextAccessor.GetRequiredUmbracoContext().Security.CurrentUser; + _dataTypeService.DeleteContainer(id, currentUser.Id); + + return Ok(); } - public HttpResponseMessage PostCreateContainer(int parentId, string name) + public IActionResult PostCreateContainer(int parentId, string name) { - var result = Services.DataTypeService.CreateContainer(parentId, name, Security.CurrentUser.Id); + var currentUser = _umbracoContextAccessor.GetRequiredUmbracoContext().Security.CurrentUser; + var result = _dataTypeService.CreateContainer(parentId, name, currentUser.Id); return result - ? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id - : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); + ? Ok(result.Result) //return the id + : throw HttpResponseException.CreateNotificationValidationErrorResponse(result.Exception.Message); } /// @@ -269,8 +264,8 @@ namespace Umbraco.Web.Editors /// /// /// - [DataTypeValidate] - public DataTypeDisplay PostSave(DataTypeSave dataType) + [TypeFilter(typeof(DataTypeValidateAttribute))] + public ActionResult PostSave(DataTypeSave dataType) { //If we've made it here, then everything has been wired up and validated by the attribute @@ -286,20 +281,22 @@ namespace Umbraco.Web.Editors dataType.PersistedDataType.Configuration = configuration; + var currentUser = _umbracoContextAccessor.GetRequiredUmbracoContext().Security.CurrentUser; // save the data type try { - Services.DataTypeService.Save(dataType.PersistedDataType, Security.CurrentUser.Id); + + _dataTypeService.Save(dataType.PersistedDataType, currentUser.Id); } catch (DuplicateNameException ex) { ModelState.AddModelError("Name", ex.Message); - throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + throw HttpResponseException.CreateValidationErrorResponse(ModelState); } // map back to display model, and return - var display = Mapper.Map(dataType.PersistedDataType); - display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/dataTypeSaved"), ""); + var display = _umbracoMapper.Map(dataType.PersistedDataType); + display.AddSuccessNotification(_localizedTextService.Localize("speechBubbles/dataTypeSaved"), ""); return display; } @@ -308,46 +305,45 @@ namespace Umbraco.Web.Editors /// /// /// - public HttpResponseMessage PostMove(MoveOrCopy move) + public IActionResult PostMove(MoveOrCopy move) { - var toMove = Services.DataTypeService.GetDataType(move.Id); + var toMove = _dataTypeService.GetDataType(move.Id); if (toMove == null) { - return Request.CreateResponse(HttpStatusCode.NotFound); + return NotFound(); } - var result = Services.DataTypeService.Move(toMove, move.ParentId); + var result = _dataTypeService.Move(toMove, move.ParentId); if (result.Success) { - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(toMove.Path, Encoding.UTF8, "text/plain"); - return response; + return Content(toMove.Path,"text/plain", Encoding.UTF8); } switch (result.Result.Result) { case MoveOperationStatusType.FailedParentNotFound: - return Request.CreateResponse(HttpStatusCode.NotFound); + return NotFound(); case MoveOperationStatusType.FailedCancelledByEvent: //returning an object of INotificationModel will ensure that any pending // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + throw HttpResponseException.CreateValidationErrorResponse(new SimpleNotificationModel()); case MoveOperationStatusType.FailedNotAllowedByPath: var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); - return Request.CreateValidationErrorResponse(notificationModel); + notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy/notAllowedByPath"), ""); + throw HttpResponseException.CreateValidationErrorResponse(notificationModel); default: throw new ArgumentOutOfRangeException(); } } - public HttpResponseMessage PostRenameContainer(int id, string name) + public IActionResult PostRenameContainer(int id, string name) { - var result = Services.DataTypeService.RenameContainer(id, name, Security.CurrentUser.Id); + var currentUser = _umbracoContextAccessor.GetRequiredUmbracoContext().Security.CurrentUser; + var result = _dataTypeService.RenameContainer(id, name, currentUser.Id); return result - ? Request.CreateResponse(HttpStatusCode.OK, result.Result) - : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); + ? Ok(result.Result) + : throw HttpResponseException.CreateNotificationValidationErrorResponse(result.Exception.Message); } /// @@ -358,7 +354,7 @@ namespace Umbraco.Web.Editors public DataTypeReferences GetReferences(int id) { var result = new DataTypeReferences(); - var usages = Services.DataTypeService.GetReferences(id); + var usages = _dataTypeService.GetReferences(id); foreach(var groupOfEntityType in usages.GroupBy(x => x.Key.EntityType)) { @@ -366,11 +362,11 @@ namespace Umbraco.Web.Editors var guidsAndPropertyAliases = groupOfEntityType.ToDictionary(i => ((GuidUdi)i.Key).Guid, i => i.Value); if (groupOfEntityType.Key == ObjectTypes.GetUdiType(UmbracoObjectTypes.DocumentType)) - result.DocumentTypes = GetContentTypeUsages(Services.ContentTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases); + result.DocumentTypes = GetContentTypeUsages(_contentTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases); else if (groupOfEntityType.Key == ObjectTypes.GetUdiType(UmbracoObjectTypes.MediaType)) - result.MediaTypes = GetContentTypeUsages(Services.MediaTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases); + result.MediaTypes = GetContentTypeUsages(_mediaTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases); else if (groupOfEntityType.Key == ObjectTypes.GetUdiType(UmbracoObjectTypes.MemberType)) - result.MemberTypes = GetContentTypeUsages(Services.MemberTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases); + result.MemberTypes = GetContentTypeUsages(_memberTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases); } return result; @@ -412,14 +408,13 @@ namespace Umbraco.Web.Editors /// /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members /// - [UmbracoApplicationAuthorize( - Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, Constants.Applications.Settings, Constants.Applications.Packages)] public IEnumerable GetAll() { - return Services.DataTypeService + return _dataTypeService .GetAll() - .Select(Mapper.Map).Where(x => x.IsSystemDataType == false); + .Select(_umbracoMapper.Map).Where(x => x.IsSystemDataType == false); } /// @@ -429,17 +424,16 @@ namespace Umbraco.Web.Editors /// /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members /// - [UmbracoTreeAuthorize( - Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + [UmbracoTreeAuthorizeAttribute(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, Constants.Applications.Settings, Constants.Applications.Packages)] public IDictionary> GetGroupedDataTypes() { - var dataTypes = Services.DataTypeService + var dataTypes = _dataTypeService .GetAll() - .Select(Mapper.Map) + .Select(_umbracoMapper.Map) .ToArray(); - var propertyEditors = Current.PropertyEditors.ToArray(); + var propertyEditors =_propertyEditorCollection.ToArray(); foreach (var dataType in dataTypes) { @@ -462,20 +456,20 @@ namespace Umbraco.Web.Editors /// /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members /// - [UmbracoTreeAuthorize( - Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + [UmbracoTreeAuthorizeAttribute(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, Constants.Applications.Settings, Constants.Applications.Packages)] + public IDictionary> GetGroupedPropertyEditors() { var datatypes = new List(); var showDeprecatedPropertyEditors = _contentSettings.ShowDeprecatedPropertyEditors; - var propertyEditors = Current.PropertyEditors + var propertyEditors =_propertyEditorCollection .Where(x=>x.IsDeprecated == false || showDeprecatedPropertyEditors); foreach (var propertyEditor in propertyEditors) { var hasPrevalues = propertyEditor.GetConfigurationEditor().Fields.Any(); - var basic = Mapper.Map(propertyEditor); + var basic = _umbracoMapper.Map(propertyEditor); basic.HasPrevalues = hasPrevalues; datatypes.Add(basic); } @@ -495,14 +489,14 @@ namespace Umbraco.Web.Editors /// /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members /// - [UmbracoTreeAuthorize( - Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + [UmbracoTreeAuthorizeAttribute(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, Constants.Applications.Settings, Constants.Applications.Packages)] + public IEnumerable GetAllPropertyEditors() { - return Current.PropertyEditors + return _propertyEditorCollection .OrderBy(x => x.Name) - .Select(Mapper.Map); + .Select(_umbracoMapper.Map); } #endregion } diff --git a/src/Umbraco.Web.BackOffice/Controllers/DetermineAmbiguousActionByPassingParameters.cs b/src/Umbraco.Web.BackOffice/Controllers/DetermineAmbiguousActionByPassingParameters.cs new file mode 100644 index 0000000000..a1f7782ea6 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/DetermineAmbiguousActionByPassingParameters.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ActionConstraints; +using Microsoft.AspNetCore.Routing; +using Umbraco.Core; + +namespace Umbraco.Web.BackOffice.Controllers +{ + public class DetermineAmbiguousActionByPassingParameters : ActionMethodSelectorAttribute + { + public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action) + { + var parameters = action.Parameters; + if (parameters.Any()) + { + var canUse = true; + foreach (var parameterDescriptor in parameters) + { + var value = routeContext.HttpContext.Request.Query[parameterDescriptor.Name]; + + if (parameterDescriptor.ParameterType == typeof(Udi)) + { + canUse &= UdiParser.TryParse(value, out _); + } + else if (parameterDescriptor.ParameterType == typeof(int)) + { + canUse &= int.TryParse(value, out _); + } + else if (parameterDescriptor.ParameterType == typeof(Guid)) + { + canUse &= Guid.TryParse(value, out _); + } + else if (parameterDescriptor.ParameterType == typeof(string)) + { + canUse = true; + } + else + { + canUse = false; + } + } + + return canUse; + } + + + return true; + } + } +} diff --git a/src/Umbraco.Web/Editors/KeepAliveController.cs b/src/Umbraco.Web.BackOffice/Controllers/KeepAliveController.cs similarity index 61% rename from src/Umbraco.Web/Editors/KeepAliveController.cs rename to src/Umbraco.Web.BackOffice/Controllers/KeepAliveController.cs index 23815e1bbe..ed8d02b7b2 100644 --- a/src/Umbraco.Web/Editors/KeepAliveController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/KeepAliveController.cs @@ -1,11 +1,15 @@ -using System.Runtime.Serialization; -using System.Web.Http; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; +using System; +using System.Runtime.Serialization; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; +using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Controllers; -namespace Umbraco.Web.Editors +namespace Umbraco.Web.BackOffice.Controllers { + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] + [IsBackOffice] public class KeepAliveController : UmbracoApiController { [OnlyLocalRequests] @@ -20,6 +24,7 @@ namespace Umbraco.Web.Editors } } + public class KeepAlivePingResult { [DataMember(Name = "success")] diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs similarity index 66% rename from src/Umbraco.Web/Editors/LanguageController.cs rename to src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs index 2bb2953227..03e4ad163d 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs @@ -1,25 +1,41 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Net; -using System.Net.Http; -using System.Web.Http; +using Microsoft.AspNetCore.Mvc; using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Mapping; using Umbraco.Core.Models; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Editors; using Language = Umbraco.Web.Models.ContentEditing.Language; -namespace Umbraco.Web.Editors +namespace Umbraco.Web.BackOffice.Controllers { /// /// Backoffice controller supporting the dashboard for language administration. /// [PluginController("UmbracoApi")] - [PrefixlessBodyModelValidator] + //[PrefixlessBodyModelValidator] public class LanguageController : UmbracoAuthorizedJsonController { + private readonly ILocalizationService _localizationService; + private readonly UmbracoMapper _umbracoMapper; + private readonly IGlobalSettings _globalSettings; + + public LanguageController(ILocalizationService localizationService, + UmbracoMapper umbracoMapper, + IGlobalSettings globalSettings) + { + _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); + _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); + } + /// /// Returns all cultures available for creating languages. /// @@ -44,30 +60,30 @@ namespace Umbraco.Web.Editors [HttpGet] public IEnumerable GetAllLanguages() { - var allLanguages = Services.LocalizationService.GetAllLanguages(); + var allLanguages = _localizationService.GetAllLanguages(); - return Mapper.Map, IEnumerable>(allLanguages); + return _umbracoMapper.Map, IEnumerable>(allLanguages); } [HttpGet] - public Language GetLanguage(int id) + public ActionResult GetLanguage(int id) { - var lang = Services.LocalizationService.GetLanguageById(id); + var lang = _localizationService.GetLanguageById(id); if (lang == null) - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + return NotFound(); - return Mapper.Map(lang); + return _umbracoMapper.Map(lang); } /// /// Deletes a language with a given ID /// - [UmbracoTreeAuthorize(Core.Constants.Trees.Languages)] + [UmbracoTreeAuthorizeAttribute(Constants.Trees.Languages)] [HttpDelete] [HttpPost] - public IHttpActionResult DeleteLanguage(int id) + public IActionResult DeleteLanguage(int id) { - var language = Services.LocalizationService.GetLanguageById(id); + var language = _localizationService.GetLanguageById(id); if (language == null) { return NotFound(); @@ -77,13 +93,13 @@ namespace Umbraco.Web.Editors if (language.IsDefault) { var message = $"Language '{language.IsoCode}' is currently set to 'default' and can not be deleted."; - throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(message)); + throw HttpResponseException.CreateNotificationValidationErrorResponse(message); } // service is happy deleting a language that's fallback for another language, // will just remove it - so no need to check here - Services.LocalizationService.Delete(language); + _localizationService.Delete(language); return Ok(); } @@ -91,15 +107,15 @@ namespace Umbraco.Web.Editors /// /// Creates or saves a language /// - [UmbracoTreeAuthorize(Core.Constants.Trees.Languages)] + [UmbracoTreeAuthorizeAttribute(Constants.Trees.Languages)] [HttpPost] public Language SaveLanguage(Language language) { if (!ModelState.IsValid) - throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + throw HttpResponseException.CreateValidationErrorResponse(ModelState); // this is prone to race conditions but the service will not let us proceed anyways - var existingByCulture = Services.LocalizationService.GetLanguageByIsoCode(language.IsoCode); + var existingByCulture = _localizationService.GetLanguageByIsoCode(language.IsoCode); // the localization service might return the generic language even when queried for specific ones (e.g. "da" when queried for "da-DK") // - we need to handle that explicitly @@ -112,10 +128,10 @@ namespace Umbraco.Web.Editors { //someone is trying to create a language that already exist ModelState.AddModelError("IsoCode", "The language " + language.IsoCode + " already exists"); - throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + throw HttpResponseException.CreateValidationErrorResponse(ModelState); } - var existingById = language.Id != default ? Services.LocalizationService.GetLanguageById(language.Id) : null; + var existingById = language.Id != default ? _localizationService.GetLanguageById(language.Id) : null; if (existingById == null) { @@ -129,11 +145,11 @@ namespace Umbraco.Web.Editors catch (CultureNotFoundException) { ModelState.AddModelError("IsoCode", "No Culture found with name " + language.IsoCode); - throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + throw HttpResponseException.CreateValidationErrorResponse(ModelState); } // create it (creating a new language cannot create a fallback cycle) - var newLang = new Core.Models.Language(GlobalSettings, culture.Name) + var newLang = new Core.Models.Language(_globalSettings, culture.Name) { CultureName = culture.DisplayName, IsDefault = language.IsDefault, @@ -141,8 +157,8 @@ namespace Umbraco.Web.Editors FallbackLanguageId = language.FallbackLanguageId }; - Services.LocalizationService.Save(newLang); - return Mapper.Map(newLang); + _localizationService.Save(newLang); + return _umbracoMapper.Map(newLang); } existingById.IsMandatory = language.IsMandatory; @@ -152,7 +168,7 @@ namespace Umbraco.Web.Editors if (existingById.IsDefault && !language.IsDefault) { ModelState.AddModelError("IsDefault", "Cannot un-default the default language."); - throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + throw HttpResponseException.CreateValidationErrorResponse(ModelState); } existingById.IsDefault = language.IsDefault; @@ -163,21 +179,21 @@ namespace Umbraco.Web.Editors // note that the service will check again, dealing with race conditions if (existingById.FallbackLanguageId.HasValue) { - var languages = Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.Id, x => x); + var languages = _localizationService.GetAllLanguages().ToDictionary(x => x.Id, x => x); if (!languages.ContainsKey(existingById.FallbackLanguageId.Value)) { ModelState.AddModelError("FallbackLanguage", "The selected fall back language does not exist."); - throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + throw HttpResponseException.CreateValidationErrorResponse(ModelState); } if (CreatesCycle(existingById, languages)) { ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {languages[existingById.FallbackLanguageId.Value].IsoCode} would create a circular path."); - throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + throw HttpResponseException.CreateValidationErrorResponse(ModelState); } } - Services.LocalizationService.Save(existingById); - return Mapper.Map(existingById); + _localizationService.Save(existingById); + return _umbracoMapper.Map(existingById); } // see LocalizationService diff --git a/src/Umbraco.Web/Editors/LogController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogController.cs similarity index 52% rename from src/Umbraco.Web/Editors/LogController.cs rename to src/Umbraco.Web.BackOffice/Controllers/LogController.cs index 7d69a4169c..9bc2be8a39 100644 --- a/src/Umbraco.Web/Editors/LogController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LogController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Mvc; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; @@ -12,12 +13,15 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Core.Strings; +using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Editors; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.Routing; using Umbraco.Web.WebApi.Filters; -namespace Umbraco.Web.Editors +namespace Umbraco.Web.BackOffice.Controllers { /// /// The API controller used for getting log history @@ -27,27 +31,34 @@ namespace Umbraco.Web.Editors { private readonly IMediaFileSystem _mediaFileSystem; private readonly IImageUrlGenerator _imageUrlGenerator; + private readonly IAuditService _auditService; + private readonly UmbracoMapper _umbracoMapper; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IUserService _userService; + private readonly AppCaches _appCaches; + private readonly ISqlContext _sqlContext; public LogController( - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger logger, - IRuntimeState runtimeState, IMediaFileSystem mediaFileSystem, - IShortStringHelper shortStringHelper, - UmbracoMapper umbracoMapper, IImageUrlGenerator imageUrlGenerator, - IPublishedUrlProvider publishedUrlProvider) - : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) - { - _mediaFileSystem = mediaFileSystem; - _imageUrlGenerator = imageUrlGenerator; - } + IAuditService auditService, + UmbracoMapper umbracoMapper, + IUmbracoContextAccessor umbracoContextAccessor, + IUserService userService, + AppCaches appCaches, + ISqlContext sqlContext) + { + _mediaFileSystem = mediaFileSystem ?? throw new ArgumentNullException(nameof(mediaFileSystem)); + _imageUrlGenerator = imageUrlGenerator ?? throw new ArgumentNullException(nameof(imageUrlGenerator)); + _auditService = auditService ?? throw new ArgumentNullException(nameof(auditService)); + _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _userService = userService ?? throw new ArgumentNullException(nameof(userService)); + _appCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); + _sqlContext = sqlContext ?? throw new ArgumentNullException(nameof(sqlContext)); + } - [UmbracoApplicationAuthorize(Core.Constants.Applications.Content, Core.Constants.Applications.Media)] + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content, Constants.Applications.Media)] public PagedResult GetPagedEntityLog(int id, int pageNumber = 1, int pageSize = 10, @@ -60,9 +71,9 @@ namespace Umbraco.Web.Editors } long totalRecords; - var dateQuery = sinceDate.HasValue ? SqlContext.Query().Where(x => x.CreateDate >= sinceDate) : null; - var result = Services.AuditService.GetPagedItemsByEntity(id, pageNumber - 1, pageSize, out totalRecords, orderDirection, customFilter: dateQuery); - var mapped = result.Select(item => Mapper.Map(item)); + var dateQuery = sinceDate.HasValue ? _sqlContext.Query().Where(x => x.CreateDate >= sinceDate) : null; + var result = _auditService.GetPagedItemsByEntity(id, pageNumber - 1, pageSize, out totalRecords, orderDirection, customFilter: dateQuery); + var mapped = result.Select(item => _umbracoMapper.Map(item)); var page = new PagedResult(totalRecords, pageNumber, pageSize) { @@ -84,10 +95,11 @@ namespace Umbraco.Web.Editors } long totalRecords; - var dateQuery = sinceDate.HasValue ? SqlContext.Query().Where(x => x.CreateDate >= sinceDate) : null; - var userId = Security.GetUserId().ResultOr(0); - var result = Services.AuditService.GetPagedItemsByUser(userId, pageNumber - 1, pageSize, out totalRecords, orderDirection, customFilter:dateQuery); - var mapped = Mapper.MapEnumerable(result); + var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + var dateQuery = sinceDate.HasValue ? _sqlContext.Query().Where(x => x.CreateDate >= sinceDate) : null; + var userId = umbracoContext.Security.GetUserId().ResultOr(0); + var result = _auditService.GetPagedItemsByUser(userId, pageNumber - 1, pageSize, out totalRecords, orderDirection, customFilter:dateQuery); + var mapped = _umbracoMapper.MapEnumerable(result); return new PagedResult(totalRecords, pageNumber, pageSize) { Items = MapAvatarsAndNames(mapped) @@ -98,9 +110,9 @@ namespace Umbraco.Web.Editors { var mappedItems = items.ToList(); var userIds = mappedItems.Select(x => x.UserId).ToArray(); - var userAvatars = Services.UserService.GetUsersById(userIds) - .ToDictionary(x => x.Id, x => x.GetUserAvatarUrls(AppCaches.RuntimeCache, _mediaFileSystem, _imageUrlGenerator)); - var userNames = Services.UserService.GetUsersById(userIds).ToDictionary(x => x.Id, x => x.Name); + var userAvatars = _userService.GetUsersById(userIds) + .ToDictionary(x => x.Id, x => x.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileSystem, _imageUrlGenerator)); + var userNames = _userService.GetUsersById(userIds).ToDictionary(x => x.Id, x => x.Name); foreach (var item in mappedItems) { if (userAvatars.TryGetValue(item.UserId, out var avatars)) diff --git a/src/Umbraco.Web/Editors/LogViewerController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs similarity index 76% rename from src/Umbraco.Web/Editors/LogViewerController.cs rename to src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs index 31b29fd426..444ebbe988 100644 --- a/src/Umbraco.Web/Editors/LogViewerController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; -using System.Web.Http; +using Microsoft.AspNetCore.Mvc; using Umbraco.Core; using Umbraco.Core.Logging.Viewer; using Umbraco.Core.Models; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Editors; -namespace Umbraco.Web.Editors +namespace Umbraco.Web.BackOffice.Controllers { /// /// Backoffice controller supporting the dashboard for viewing logs with some simple graphs & filtering @@ -33,60 +34,60 @@ namespace Umbraco.Web.Editors } [HttpGet] - public bool GetCanViewLogs([FromUri] DateTime? startDate = null,[FromUri] DateTime? endDate = null) + public bool GetCanViewLogs([FromQuery] DateTime? startDate = null,[FromQuery] DateTime? endDate = null) { var logTimePeriod = GetTimePeriod(startDate, endDate); return CanViewLogs(logTimePeriod); } [HttpGet] - public int GetNumberOfErrors([FromUri] DateTime? startDate = null,[FromUri] DateTime? endDate = null) + public int GetNumberOfErrors([FromQuery] DateTime? startDate = null,[FromQuery] DateTime? endDate = null) { var logTimePeriod = GetTimePeriod(startDate, endDate); //We will need to stop the request if trying to do this on a 1GB file if (CanViewLogs(logTimePeriod) == false) { - throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Unable to view logs, due to size")); + throw HttpResponseException.CreateNotificationValidationErrorResponse("Unable to view logs, due to size"); } return _logViewer.GetNumberOfErrors(logTimePeriod); } [HttpGet] - public LogLevelCounts GetLogLevelCounts([FromUri] DateTime? startDate = null,[FromUri] DateTime? endDate = null) + public LogLevelCounts GetLogLevelCounts([FromQuery] DateTime? startDate = null,[FromQuery] DateTime? endDate = null) { var logTimePeriod = GetTimePeriod(startDate, endDate); //We will need to stop the request if trying to do this on a 1GB file if (CanViewLogs(logTimePeriod) == false) { - throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Unable to view logs, due to size")); + throw HttpResponseException.CreateNotificationValidationErrorResponse("Unable to view logs, due to size"); } return _logViewer.GetLogLevelCounts(logTimePeriod); } [HttpGet] - public IEnumerable GetMessageTemplates([FromUri] DateTime? startDate = null,[FromUri] DateTime? endDate = null) + public IEnumerable GetMessageTemplates([FromQuery] DateTime? startDate = null,[FromQuery] DateTime? endDate = null) { var logTimePeriod = GetTimePeriod(startDate, endDate); //We will need to stop the request if trying to do this on a 1GB file if (CanViewLogs(logTimePeriod) == false) { - throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Unable to view logs, due to size")); + throw HttpResponseException.CreateNotificationValidationErrorResponse("Unable to view logs, due to size"); } return _logViewer.GetMessageTemplates(logTimePeriod); } [HttpGet] - public PagedResult GetLogs(string orderDirection = "Descending", int pageNumber = 1, string filterExpression = null, [FromUri]string[] logLevels = null, [FromUri] DateTime? startDate = null,[FromUri] DateTime? endDate = null) + public PagedResult GetLogs(string orderDirection = "Descending", int pageNumber = 1, string filterExpression = null, [FromQuery]string[] logLevels = null, [FromQuery] DateTime? startDate = null,[FromQuery] DateTime? endDate = null) { var logTimePeriod = GetTimePeriod(startDate, endDate); //We will need to stop the request if trying to do this on a 1GB file if (CanViewLogs(logTimePeriod) == false) { - throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Unable to view logs, due to size")); + throw HttpResponseException.CreateNotificationValidationErrorResponse("Unable to view logs, due to size"); } var direction = orderDirection == "Descending" ? Direction.Descending : Direction.Ascending; diff --git a/src/Umbraco.Web/Editors/PackageController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs similarity index 50% rename from src/Umbraco.Web/Editors/PackageController.cs rename to src/Umbraco.Web.BackOffice/Controllers/PackageController.cs index 6dc913237d..23ba2e5771 100644 --- a/src/Umbraco.Web/Editors/PackageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs @@ -1,65 +1,54 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; using System.Text; using System.Web; -using System.Web.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; using Semver; using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; +using Umbraco.Core.Hosting; using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Mapping; using Umbraco.Core.Models.Packaging; -using Umbraco.Core.Persistence; using Umbraco.Core.Services; -using Umbraco.Core.Strings; -using Umbraco.Web.Mvc; -using Umbraco.Web.Routing; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; +using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Editors; -namespace Umbraco.Web.Editors +namespace Umbraco.Web.BackOffice.Controllers { /// /// A controller used for managing packages in the back office /// [PluginController("UmbracoApi")] - [SerializeVersion] - [UmbracoApplicationAuthorize(Core.Constants.Applications.Packages)] + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Packages)] public class PackageController : UmbracoAuthorizedJsonController { - private readonly IIOHelper _ioHelper; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IPackagingService _packagingService; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; public PackageController( - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger logger, - IRuntimeState runtimeState, - IShortStringHelper shortStringHelper, - UmbracoMapper umbracoMapper, - IIOHelper ioHelper, - IPublishedUrlProvider publishedUrlProvider) - : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) + IHostingEnvironment hostingEnvironment, + IPackagingService packagingService, + IUmbracoContextAccessor umbracoContextAccessor) { - _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + _packagingService = packagingService ?? throw new ArgumentNullException(nameof(packagingService)); + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); } public IEnumerable GetCreatedPackages() { - return Services.PackagingService.GetAllCreatedPackages(); + return _packagingService.GetAllCreatedPackages(); } public PackageDefinition GetCreatedPackageById(int id) { - var package = Services.PackagingService.GetCreatedPackageById(id); + var package = _packagingService.GetCreatedPackageById(id); if (package == null) throw new HttpResponseException(HttpStatusCode.NotFound); @@ -79,17 +68,16 @@ namespace Umbraco.Web.Editors public PackageDefinition PostSavePackage(PackageDefinition model) { if (ModelState.IsValid == false) - throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + throw HttpResponseException.CreateValidationErrorResponse(ModelState); //save it - if (!Services.PackagingService.SaveCreatedPackage(model)) - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( + if (!_packagingService.SaveCreatedPackage(model)) + throw HttpResponseException.CreateNotificationValidationErrorResponse( model.Id == default ? $"A package with the name {model.Name} already exists" - : $"The package with id {model.Id} was not found")); + : $"The package with id {model.Id} was not found"); - Services.PackagingService.ExportCreatedPackage(model); + _packagingService.ExportCreatedPackage(model); //the packagePath will be on the model return model; @@ -102,55 +90,48 @@ namespace Umbraco.Web.Editors /// [HttpPost] [HttpDelete] - public IHttpActionResult DeleteCreatedPackage(int packageId) + public IActionResult DeleteCreatedPackage(int packageId) { - Services.PackagingService.DeleteCreatedPackage(packageId, Security.GetUserId().ResultOr(0)); + var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + _packagingService.DeleteCreatedPackage(packageId, umbracoContext.Security.GetUserId().ResultOr(0)); return Ok(); } [HttpGet] - public HttpResponseMessage DownloadCreatedPackage(int id) + public IActionResult DownloadCreatedPackage(int id) { - var package = Services.PackagingService.GetCreatedPackageById(id); + var package = _packagingService.GetCreatedPackageById(id); if (package == null) - return Request.CreateResponse(HttpStatusCode.NotFound); + return NotFound(); - var fullPath = _ioHelper.MapPath(package.PackagePath); - if (!File.Exists(fullPath)) - return Request.CreateNotificationValidationErrorResponse("No file found for path " + package.PackagePath); + var fullPath = _hostingEnvironment.MapPathContentRoot(package.PackagePath); + if (!System.IO.File.Exists(fullPath)) + throw HttpResponseException.CreateNotificationValidationErrorResponse("No file found for path " + package.PackagePath); var fileName = Path.GetFileName(package.PackagePath); var encoding = Encoding.UTF8; - var response = new HttpResponseMessage + var cd = new System.Net.Mime.ContentDisposition { - Content = new StreamContent(File.OpenRead(fullPath)) - { - Headers = - { - ContentDisposition = new ContentDispositionHeaderValue("attachment") - { - FileName = HttpUtility.UrlEncode(fileName, encoding) - }, - ContentType = new MediaTypeHeaderValue("application/octet-stream") - { - CharSet = encoding.WebName - } - } - } + FileName = HttpUtility.UrlEncode(fileName, encoding), + Inline = false // false = prompt the user for downloading; true = browser to try to show the file inline }; - + Response.Headers.Add("Content-Disposition", cd.ToString()); // Set custom header so umbRequestHelper.downloadFile can save the correct filename - response.Headers.Add("x-filename", HttpUtility.UrlEncode(fileName, encoding)); + Response.Headers.Add("x-filename", HttpUtility.UrlEncode(fileName, encoding)); + return new FileStreamResult(System.IO.File.OpenRead(fullPath), new MediaTypeHeaderValue("application/octet-stream") + { + Charset = encoding.WebName, + + }); - return response; } public PackageDefinition GetInstalledPackageById(int id) { - var pack = Services.PackagingService.GetInstalledPackageById(id); + var pack = _packagingService.GetInstalledPackageById(id); if (pack == null) throw new HttpResponseException(HttpStatusCode.NotFound); return pack; } @@ -161,7 +142,7 @@ namespace Umbraco.Web.Editors /// public IEnumerable GetInstalled() { - return Services.PackagingService.GetAllInstalledPackages() + return _packagingService.GetAllInstalledPackages() .GroupBy( //group by name x => x.Name, diff --git a/src/Umbraco.Web/Editors/PackageInstallController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs similarity index 62% rename from src/Umbraco.Web/Editors/PackageInstallController.cs rename to src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs index 650050ff66..5330d4466f 100644 --- a/src/Umbraco.Web/Editors/PackageInstallController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs @@ -1,69 +1,66 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; -using System.Net.Http; using System.Threading.Tasks; -using System.Web.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Semver; using Umbraco.Core; -using Umbraco.Core.Cache; using Umbraco.Core.Configuration; -using Umbraco.Core.IO; +using Umbraco.Core.Hosting; using Umbraco.Core.Logging; -using Umbraco.Core.Mapping; using Umbraco.Core.Models.Editors; using Umbraco.Core.Models.Packaging; using Umbraco.Net; using Umbraco.Core.Packaging; -using Umbraco.Core.Persistence; using Umbraco.Core.Services; -using Umbraco.Core.Strings; using Umbraco.Core.WebAssets; +using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Editors; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Umbraco.Web.Routing; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; -using File = System.IO.File; -namespace Umbraco.Web.Editors +namespace Umbraco.Web.BackOffice.Controllers { /// /// A controller used for installing packages and managing all of the data in the packages section in the back office /// [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorize(Core.Constants.Applications.Packages)] + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Packages)] public class PackageInstallController : UmbracoAuthorizedJsonController { private readonly IUmbracoVersion _umbracoVersion; - private readonly IIOHelper _ioHelper; + private readonly IHostingEnvironment _hostingEnvironment; private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; private readonly IRuntimeMinifier _runtimeMinifier; + private readonly IPackagingService _packagingService; + private readonly ILogger _logger; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly ILocalizedTextService _localizedTextService; public PackageInstallController( - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger logger, - IRuntimeState runtimeState, - IShortStringHelper shortStringHelper, IUmbracoVersion umbracoVersion, - UmbracoMapper umbracoMapper, - IIOHelper ioHelper, - IPublishedUrlProvider publishedUrlProvider, + IHostingEnvironment hostingEnvironment, IUmbracoApplicationLifetime umbracoApplicationLifetime, - IRuntimeMinifier runtimeMinifier) - : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) + IRuntimeMinifier runtimeMinifier, + IPackagingService packagingService, + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + ILocalizedTextService localizedTextService) { - _umbracoVersion = umbracoVersion; - _ioHelper = ioHelper; - _umbracoApplicationLifetime = umbracoApplicationLifetime; - _runtimeMinifier = runtimeMinifier; + _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + _umbracoApplicationLifetime = umbracoApplicationLifetime ?? throw new ArgumentNullException(nameof(umbracoApplicationLifetime)); + _runtimeMinifier = runtimeMinifier ?? throw new ArgumentNullException(nameof(runtimeMinifier)); + _packagingService = packagingService ?? throw new ArgumentNullException(nameof(packagingService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); } /// @@ -73,9 +70,9 @@ namespace Umbraco.Web.Editors /// /// [HttpPost] - public IHttpActionResult ValidateInstalled(string name, string version) + public IActionResult ValidateInstalled(string name, string version) { - var installType = Services.PackagingService.GetPackageInstallType(name, SemVersion.Parse(version), out _); + var installType = _packagingService.GetPackageInstallType(name, SemVersion.Parse(version), out _); if (installType == PackageInstallType.AlreadyInstalled) return BadRequest(); @@ -84,26 +81,28 @@ namespace Umbraco.Web.Editors } [HttpPost] - public IHttpActionResult Uninstall(int packageId) + public IActionResult Uninstall(int packageId) { try { - var package = Services.PackagingService.GetInstalledPackageById(packageId); + + var package = _packagingService.GetInstalledPackageById(packageId); if (package == null) return NotFound(); - var summary = Services.PackagingService.UninstallPackage(package.Name, Security.GetUserId().ResultOr(0)); + var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + var summary = _packagingService.UninstallPackage(package.Name, umbracoContext.Security.GetUserId().ResultOr(0)); //now get all other packages by this name since we'll uninstall all versions - foreach (var installed in Services.PackagingService.GetAllInstalledPackages() + foreach (var installed in _packagingService.GetAllInstalledPackages() .Where(x => x.Name == package.Name && x.Id != package.Id)) { //remove from the xml - Services.PackagingService.DeleteInstalledPackage(installed.Id, Security.GetUserId().ResultOr(0)); + _packagingService.DeleteInstalledPackage(installed.Id, umbracoContext.Security.GetUserId().ResultOr(0)); } } catch (Exception ex) { - Logger.Error(ex, "Failed to uninstall."); + _logger.Error(ex, "Failed to uninstall."); throw; } @@ -114,9 +113,9 @@ namespace Umbraco.Web.Editors private void PopulateFromPackageData(LocalPackageInstallModel model) { - var zipFile = new FileInfo(Path.Combine(_ioHelper.MapPath(Core.Constants.SystemDirectories.Packages), model.ZipFileName)); + var zipFile = new FileInfo(Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages), model.ZipFileName)); - var ins = Services.PackagingService.GetCompiledPackageInfo(zipFile); + var ins = _packagingService.GetCompiledPackageInfo(zipFile); model.Name = ins.Name; model.Author = ins.Author; @@ -150,22 +149,11 @@ namespace Umbraco.Web.Editors } [HttpPost] - [FileUploadCleanupFilter(false)] - public async Task UploadLocalPackage() + public async Task> UploadLocalPackage(List file) { - if (Request.Content.IsMimeMultipartContent() == false) - throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); - - var root = _ioHelper.MapPath(Core.Constants.SystemDirectories.TempFileUploads); - //ensure it exists - Directory.CreateDirectory(root); - var provider = new MultipartFormDataStreamProvider(root); - - var result = await Request.Content.ReadAsMultipartAsync(provider); - //must have a file - if (result.FileData.Count == 0) - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + if (file.Count == 0) + return NotFound(); var model = new LocalPackageInstallModel { @@ -174,38 +162,36 @@ namespace Umbraco.Web.Editors }; //get the files - foreach (var file in result.FileData) + foreach (var formFile in file) { - var fileName = file.Headers.ContentDisposition.FileName.Trim('\"'); + var fileName = formFile.FileName.Trim('\"'); var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); if (ext.InvariantEquals("zip") || ext.InvariantEquals("umb")) { //we always save package files to /App_Data/packages/package-guid.umb for processing as a standard so lets copy. - var packagesFolder = _ioHelper.MapPath(Core.Constants.SystemDirectories.Packages); + var packagesFolder = _hostingEnvironment.MapPathContentRoot(Core.Constants.SystemDirectories.Packages); Directory.CreateDirectory(packagesFolder); var packageFile = Path.Combine(packagesFolder, model.PackageGuid + ".umb"); - File.Copy(file.LocalFileName, packageFile); + + using (var stream = System.IO.File.Create(packageFile)) + { + await formFile.CopyToAsync(stream); + } model.ZipFileName = Path.GetFileName(packageFile); - //add to the outgoing model so that all temp files are cleaned up - model.UploadedFiles.Add(new ContentPropertyFile - { - TempFilePath = file.LocalFileName - }); - //Populate the model from the metadata in the package file (zip file) PopulateFromPackageData(model); - var installType = Services.PackagingService.GetPackageInstallType(model.Name, SemVersion.Parse(model.Version), out var alreadyInstalled); + var installType = _packagingService.GetPackageInstallType(model.Name, SemVersion.Parse(model.Version), out var alreadyInstalled); if (installType == PackageInstallType.AlreadyInstalled) { //this package is already installed - throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("packager/packageAlreadyInstalled"))); + throw HttpResponseException.CreateNotificationValidationErrorResponse( + _localizedTextService.Localize("packager/packageAlreadyInstalled")); } model.OriginalVersion = installType == PackageInstallType.Upgrade ? alreadyInstalled.Version : null; @@ -214,8 +200,8 @@ namespace Umbraco.Web.Editors else { model.Notifications.Add(new BackOfficeNotification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), - Services.TextService.Localize("media/disallowedFileType"), + _localizedTextService.Localize("speechBubbles/operationFailedHeader"), + _localizedTextService.Localize("media/disallowedFileType"), NotificationStyle.Warning)); } @@ -235,12 +221,13 @@ namespace Umbraco.Web.Editors { //Default path string fileName = packageGuid + ".umb"; - if (File.Exists(Path.Combine(_ioHelper.MapPath(Core.Constants.SystemDirectories.Packages), fileName)) == false) + if (System.IO.File.Exists(Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages), fileName)) == false) { - var packageFile = await Services.PackagingService.FetchPackageFileAsync( + var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + var packageFile = await _packagingService.FetchPackageFileAsync( Guid.Parse(packageGuid), _umbracoVersion.Current, - Security.GetUserId().ResultOr(0)); + umbracoContext.Security.GetUserId().ResultOr(0)); fileName = packageFile.Name; } @@ -254,12 +241,12 @@ namespace Umbraco.Web.Editors //Populate the model from the metadata in the package file (zip file) PopulateFromPackageData(model); - var installType = Services.PackagingService.GetPackageInstallType(model.Name, SemVersion.Parse(model.Version), out var alreadyInstalled); + var installType = _packagingService.GetPackageInstallType(model.Name, SemVersion.Parse(model.Version), out var alreadyInstalled); if (installType == PackageInstallType.AlreadyInstalled) { - throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("packager/packageAlreadyInstalled"))); + throw HttpResponseException.CreateNotificationValidationErrorResponse( + _localizedTextService.Localize("packager/packageAlreadyInstalled")); } model.OriginalVersion = installType == PackageInstallType.Upgrade ? alreadyInstalled.Version : null; @@ -275,20 +262,20 @@ namespace Umbraco.Web.Editors [HttpPost] public PackageInstallModel Import(PackageInstallModel model) { - var zipFile = new FileInfo(Path.Combine(_ioHelper.MapPath(Core.Constants.SystemDirectories.Packages), model.ZipFileName)); + var zipFile = new FileInfo(Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages), model.ZipFileName)); - var packageInfo = Services.PackagingService.GetCompiledPackageInfo(zipFile); + var packageInfo = _packagingService.GetCompiledPackageInfo(zipFile); //now we need to check for version comparison if (packageInfo.UmbracoVersionRequirementsType == RequirementsType.Strict) { var packageMinVersion = packageInfo.UmbracoVersion; if (_umbracoVersion.Current < packageMinVersion) - throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("packager/targetVersionMismatch", new[] {packageMinVersion.ToString()}))); + throw HttpResponseException.CreateNotificationValidationErrorResponse( + _localizedTextService.Localize("packager/targetVersionMismatch", new[] {packageMinVersion.ToString()})); } - var installType = Services.PackagingService.GetPackageInstallType(packageInfo.Name, SemVersion.Parse(packageInfo.Version), out var alreadyInstalled); + var installType = _packagingService.GetPackageInstallType(packageInfo.Name, SemVersion.Parse(packageInfo.Version), out var alreadyInstalled); var packageDefinition = PackageDefinition.FromCompiledPackage(packageInfo); packageDefinition.PackagePath = zipFile.FullName; @@ -302,8 +289,8 @@ namespace Umbraco.Web.Editors case PackageInstallType.Upgrade: //save to the installedPackages.config, this will create a new entry with a new Id - if (!Services.PackagingService.SaveInstalledPackage(packageDefinition)) - throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Could not save the package")); + if (!_packagingService.SaveInstalledPackage(packageDefinition)) + throw HttpResponseException.CreateNotificationValidationErrorResponse("Could not save the package"); model.Id = packageDefinition.Id; break; @@ -323,12 +310,12 @@ namespace Umbraco.Web.Editors [HttpPost] public PackageInstallModel InstallFiles(PackageInstallModel model) { - var definition = Services.PackagingService.GetInstalledPackageById(model.Id); + var definition = _packagingService.GetInstalledPackageById(model.Id); if (definition == null) throw new InvalidOperationException("Not package definition found with id " + model.Id); var zipFile = new FileInfo(definition.PackagePath); - - var installedFiles = Services.PackagingService.InstallCompiledPackageFiles(definition, zipFile, Security.GetUserId().ResultOr(0)); + var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + var installedFiles = _packagingService.InstallCompiledPackageFiles(definition, zipFile, umbracoContext.Security.GetUserId().ResultOr(0)); //set a restarting marker and reset the app pool _umbracoApplicationLifetime.Restart(); @@ -356,12 +343,12 @@ namespace Umbraco.Web.Editors [HttpPost] public PackageInstallModel InstallData(PackageInstallModel model) { - var definition = Services.PackagingService.GetInstalledPackageById(model.Id); + var definition = _packagingService.GetInstalledPackageById(model.Id); if (definition == null) throw new InvalidOperationException("Not package definition found with id " + model.Id); var zipFile = new FileInfo(definition.PackagePath); - - var installSummary = Services.PackagingService.InstallCompiledPackageData(definition, zipFile, Security.GetUserId().ResultOr(0)); + var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + var installSummary = _packagingService.InstallCompiledPackageData(definition, zipFile, umbracoContext.Security.GetUserId().ResultOr(0)); return model; } @@ -374,12 +361,12 @@ namespace Umbraco.Web.Editors [HttpPost] public PackageInstallResult CleanUp(PackageInstallModel model) { - var definition = Services.PackagingService.GetInstalledPackageById(model.Id); + var definition = _packagingService.GetInstalledPackageById(model.Id); if (definition == null) throw new InvalidOperationException("Not package definition found with id " + model.Id); var zipFile = new FileInfo(definition.PackagePath); - var packageInfo = Services.PackagingService.GetCompiledPackageInfo(zipFile); + var packageInfo = _packagingService.GetCompiledPackageInfo(zipFile); zipFile.Delete(); diff --git a/src/Umbraco.Web/Editors/PublishedSnapshotCacheStatusController.cs b/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs similarity index 68% rename from src/Umbraco.Web/Editors/PublishedSnapshotCacheStatusController.cs rename to src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs index 6a701fdcfa..1a0e3457ca 100644 --- a/src/Umbraco.Web/Editors/PublishedSnapshotCacheStatusController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs @@ -1,19 +1,24 @@ using System; -using System.Web.Http; +using System.Reflection.Metadata; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; using Umbraco.Web.Cache; -using Umbraco.Web.Composing; +using Umbraco.Web.Common.Attributes; using Umbraco.Web.PublishedCache; -using Umbraco.Web.WebApi; -namespace Umbraco.Web.Editors +namespace Umbraco.Web.BackOffice.Controllers { + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] + [IsBackOffice] public class PublishedSnapshotCacheStatusController : UmbracoAuthorizedApiController { private readonly IPublishedSnapshotService _publishedSnapshotService; + private readonly DistributedCache _distributedCache; - public PublishedSnapshotCacheStatusController(IPublishedSnapshotService publishedSnapshotService) + public PublishedSnapshotCacheStatusController(IPublishedSnapshotService publishedSnapshotService, DistributedCache distributedCache) { _publishedSnapshotService = publishedSnapshotService ?? throw new ArgumentNullException(nameof(publishedSnapshotService)); + _distributedCache = distributedCache; } [HttpPost] @@ -40,7 +45,7 @@ namespace Umbraco.Web.Editors [HttpPost] public void ReloadCache() { - Current.DistributedCache.RefreshAllPublishedSnapshot(); + _distributedCache.RefreshAllPublishedSnapshot(); } } } diff --git a/src/Umbraco.Web/Editors/PublishedStatusController.cs b/src/Umbraco.Web.BackOffice/Controllers/PublishedStatusController.cs similarity index 91% rename from src/Umbraco.Web/Editors/PublishedStatusController.cs rename to src/Umbraco.Web.BackOffice/Controllers/PublishedStatusController.cs index 5ed70c4811..f63c2d5e6a 100644 --- a/src/Umbraco.Web/Editors/PublishedStatusController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PublishedStatusController.cs @@ -1,9 +1,8 @@ using System; -using System.Web.Http; +using Microsoft.AspNetCore.Mvc; using Umbraco.Web.PublishedCache; -using Umbraco.Web.WebApi; -namespace Umbraco.Web.Editors +namespace Umbraco.Web.BackOffice.Controllers { public class PublishedStatusController : UmbracoAuthorizedApiController { diff --git a/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs similarity index 56% rename from src/Umbraco.Web/Editors/RedirectUrlManagementController.cs rename to src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs index d1bb3f873c..c749e85839 100644 --- a/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs @@ -1,30 +1,42 @@ using System; -using System.Web.Http; using System.Xml; -using System.Linq; using System.Security; +using Microsoft.AspNetCore.Mvc; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using File = System.IO.File; using Umbraco.Core; -using Umbraco.Web.Composing; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Hosting; +using Umbraco.Core.Mapping; +using Umbraco.Core.Services; +using Umbraco.Web.Common.Attributes; -namespace Umbraco.Web.Editors +namespace Umbraco.Web.BackOffice.Controllers { [PluginController("UmbracoApi")] public class RedirectUrlManagementController : UmbracoAuthorizedApiController { private readonly ILogger _logger; private readonly IWebRoutingSettings _webRoutingSettings; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IRedirectUrlService _redirectUrlService; + private readonly UmbracoMapper _umbracoMapper; + private readonly IHostingEnvironment _hostingEnvironment; - public RedirectUrlManagementController(ILogger logger, IWebRoutingSettings webRoutingSettings) + public RedirectUrlManagementController(ILogger logger, + IWebRoutingSettings webRoutingSettings, + IUmbracoContextAccessor umbracoContextAccessor, + IRedirectUrlService redirectUrlService, + UmbracoMapper umbracoMapper, + IHostingEnvironment hostingEnvironment) { - _logger = logger; - _webRoutingSettings = webRoutingSettings; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _webRoutingSettings = webRoutingSettings ?? throw new ArgumentNullException(nameof(webRoutingSettings)); + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _redirectUrlService = redirectUrlService ?? throw new ArgumentNullException(nameof(redirectUrlService)); + _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); } /// @@ -32,10 +44,11 @@ namespace Umbraco.Web.Editors /// /// [HttpGet] - public IHttpActionResult GetEnableState() + public IActionResult GetEnableState() { var enabled = _webRoutingSettings.DisableRedirectUrlTracking == false; - var userIsAdmin = UmbracoContext.Security.CurrentUser.IsAdmin(); + var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + var userIsAdmin = umbracoContext.Security.CurrentUser.IsAdmin(); return Ok(new { enabled, userIsAdmin }); } @@ -44,14 +57,13 @@ namespace Umbraco.Web.Editors public RedirectUrlSearchResult SearchRedirectUrls(string searchTerm, int page = 0, int pageSize = 10) { var searchResult = new RedirectUrlSearchResult(); - var redirectUrlService = Services.RedirectUrlService; long resultCount; var redirects = string.IsNullOrWhiteSpace(searchTerm) - ? redirectUrlService.GetAllRedirectUrls(page, pageSize, out resultCount) - : redirectUrlService.SearchRedirectUrls(searchTerm, page, pageSize, out resultCount); + ? _redirectUrlService.GetAllRedirectUrls(page, pageSize, out resultCount) + : _redirectUrlService.SearchRedirectUrls(searchTerm, page, pageSize, out resultCount); - searchResult.SearchResults = Mapper.MapEnumerable(redirects); + searchResult.SearchResults = _umbracoMapper.MapEnumerable(redirects); searchResult.TotalCount = resultCount; searchResult.CurrentPage = page; searchResult.PageCount = ((int)resultCount + pageSize - 1) / pageSize; @@ -71,43 +83,40 @@ namespace Umbraco.Web.Editors var redirectsResult = new RedirectUrlSearchResult(); if (UdiParser.TryParse(contentUdi, out GuidUdi guidIdi)) { - var redirectUrlService = Services.RedirectUrlService; - var redirects = redirectUrlService.GetContentRedirectUrls(guidIdi.Guid); - var mapped = Mapper.MapEnumerable(redirects); + + var redirects = _redirectUrlService.GetContentRedirectUrls(guidIdi.Guid); + var mapped = _umbracoMapper.MapEnumerable(redirects); redirectsResult.SearchResults = mapped; //not doing paging 'yet' - redirectsResult.TotalCount = mapped.Count(); + redirectsResult.TotalCount = mapped.Count; redirectsResult.CurrentPage = 1; redirectsResult.PageCount = 1; } return redirectsResult; } [HttpPost] - public IHttpActionResult DeleteRedirectUrl(Guid id) + public IActionResult DeleteRedirectUrl(Guid id) { - var redirectUrlService = Services.RedirectUrlService; - redirectUrlService.Delete(id); + _redirectUrlService.Delete(id); return Ok(); } [HttpPost] - public IHttpActionResult ToggleUrlTracker(bool disable) + public IActionResult ToggleUrlTracker(bool disable) { - var userIsAdmin = UmbracoContext.Security.CurrentUser.IsAdmin(); + var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + var userIsAdmin = umbracoContext.Security.CurrentUser.IsAdmin(); if (userIsAdmin == false) { var errorMessage = "User is not a member of the administrators group and so is not allowed to toggle the URL tracker"; _logger.Debug(errorMessage); throw new SecurityException(errorMessage); } - - var httpContext = TryGetHttpContext(); - if (httpContext.Success == false) throw new InvalidOperationException("Cannot acquire HttpContext"); - var configFilePath = httpContext.Result.Server.MapPath("~/config/umbracoSettings.config"); + var configFilePath =_hostingEnvironment.MapPathContentRoot("~/config/umbracoSettings.config"); var action = disable ? "disable" : "enable"; - if (File.Exists(configFilePath) == false) + if (System.IO.File.Exists(configFilePath) == false) return BadRequest($"Couldn't {action} URL Tracker, the umbracoSettings.config file does not exist."); var umbracoConfig = new XmlDocument { PreserveWhitespace = true }; diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs new file mode 100644 index 0000000000..686afe284b --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; +using Umbraco.Core.Mapping; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.Common.ActionsResults; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Editors; +using Umbraco.Web.Models.ContentEditing; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.BackOffice.Controllers +{ + [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)] + public class RelationController : UmbracoAuthorizedJsonController + { + private readonly UmbracoMapper _umbracoMapper; + private readonly IRelationService _relationService; + + public RelationController(UmbracoMapper umbracoMapper, + IRelationService relationService) + { + _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + _relationService = relationService ?? throw new ArgumentNullException(nameof(relationService)); + } + + public RelationDisplay GetById(int id) + { + return _umbracoMapper.Map(_relationService.GetById(id)); + } + + //[EnsureUserPermissionForContent("childId")] + public IEnumerable GetByChildId(int childId, string relationTypeAlias = "") + { + var relations = _relationService.GetByChildId(childId).ToArray(); + + if (relations.Any() == false) + { + return Enumerable.Empty(); + } + + if (string.IsNullOrWhiteSpace(relationTypeAlias) == false) + { + return + _umbracoMapper.MapEnumerable( + relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))); + } + + return _umbracoMapper.MapEnumerable(relations); + } + + [HttpDelete] + [HttpPost] + public IActionResult DeleteById(int id) + { + var foundRelation = _relationService.GetById(id); + + if (foundRelation == null) + { + return new UmbracoProblemResult("No relation found with the specified id", HttpStatusCode.NotFound); + } + + _relationService.Delete(foundRelation); + + return Ok(); + } + } +} diff --git a/src/Umbraco.Web/Editors/RelationTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs similarity index 61% rename from src/Umbraco.Web/Editors/RelationTypeController.cs rename to src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs index b5b983c9ff..f679dd6b8e 100644 --- a/src/Umbraco.Web/Editors/RelationTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs @@ -1,50 +1,49 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; -using System.Web.Http; -using System.Linq; +using Microsoft.AspNetCore.Mvc; using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; using Umbraco.Core.Mapping; -using Umbraco.Web.Routing; +using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Editors; -namespace Umbraco.Web.Editors +namespace Umbraco.Web.BackOffice.Controllers { /// /// The API controller for editing relation types. /// [PluginController("UmbracoApi")] - [UmbracoTreeAuthorize(Constants.Trees.RelationTypes)] - [EnableOverrideAuthorization] + [UmbracoTreeAuthorizeAttribute(Constants.Trees.RelationTypes)] public class RelationTypeController : BackOfficeNotificationsController { + private readonly ILogger _logger; + private readonly UmbracoMapper _umbracoMapper; + private readonly IRelationService _relationService; + private readonly IShortStringHelper _shortStringHelper; + public RelationTypeController( - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger logger, - IRuntimeState runtimeState, - IShortStringHelper shortStringHelper, + ILogger logger, UmbracoMapper umbracoMapper, - IPublishedUrlProvider publishedUrlProvider) - : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) + IRelationService relationService, + IShortStringHelper shortStringHelper) { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + _relationService = relationService ?? throw new ArgumentNullException(nameof(relationService)); + _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); } + /// /// Gets a relation type by ID. /// @@ -52,14 +51,14 @@ namespace Umbraco.Web.Editors /// Returns the . public RelationTypeDisplay GetById(int id) { - var relationType = Services.RelationService.GetRelationTypeById(id); + var relationType = _relationService.GetRelationTypeById(id); if (relationType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } - var display = Mapper.Map(relationType); + var display = _umbracoMapper.Map(relationType); return display; } @@ -73,11 +72,11 @@ namespace Umbraco.Web.Editors } // Ordering do we need to pass through? - var relations = Services.RelationService.GetPagedByRelationTypeId(id, pageNumber -1, pageSize, out long totalRecords); + var relations = _relationService.GetPagedByRelationTypeId(id, pageNumber -1, pageSize, out long totalRecords); return new PagedResult(totalRecords, pageNumber, pageSize) { - Items = relations.Select(x => Mapper.Map(x)) + Items = relations.Select(x => _umbracoMapper.Map(x)) }; } @@ -109,20 +108,20 @@ namespace Umbraco.Web.Editors /// /// The relation type to create. /// A containing the persisted relation type's ID. - public HttpResponseMessage PostCreate(RelationTypeSave relationType) + public int PostCreate(RelationTypeSave relationType) { - var relationTypePersisted = new RelationType(relationType.Name, relationType.Name.ToSafeAlias(ShortStringHelper, true), relationType.IsBidirectional, relationType.ChildObjectType, relationType.ParentObjectType); + var relationTypePersisted = new RelationType(relationType.Name, relationType.Name.ToSafeAlias(_shortStringHelper, true), relationType.IsBidirectional, relationType.ChildObjectType, relationType.ParentObjectType); try { - Services.RelationService.Save(relationTypePersisted); + _relationService.Save(relationTypePersisted); - return Request.CreateResponse(HttpStatusCode.OK, relationTypePersisted.Id); + return relationTypePersisted.Id; } catch (Exception ex) { - Logger.Error(GetType(), ex, "Error creating relation type with {Name}", relationType.Name); - return Request.CreateNotificationValidationErrorResponse("Error creating relation type."); + _logger.Error(GetType(), ex, "Error creating relation type with {Name}", relationType.Name); + throw HttpResponseException.CreateNotificationValidationErrorResponse("Error creating relation type."); } } @@ -133,27 +132,27 @@ namespace Umbraco.Web.Editors /// A display object containing the updated relation type. public RelationTypeDisplay PostSave(RelationTypeSave relationType) { - var relationTypePersisted = Services.RelationService.GetRelationTypeById(relationType.Key); + var relationTypePersisted = _relationService.GetRelationTypeById(relationType.Key); if (relationTypePersisted == null) { - throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Relation type does not exist")); + throw HttpResponseException.CreateNotificationValidationErrorResponse("Relation type does not exist"); } - Mapper.Map(relationType, relationTypePersisted); + _umbracoMapper.Map(relationType, relationTypePersisted); try { - Services.RelationService.Save(relationTypePersisted); - var display = Mapper.Map(relationTypePersisted); + _relationService.Save(relationTypePersisted); + var display = _umbracoMapper.Map(relationTypePersisted); display.AddSuccessNotification("Relation type saved", ""); return display; } catch (Exception ex) { - Logger.Error(GetType(), ex, "Error saving relation type with {Id}", relationType.Id); - throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Something went wrong when saving the relation type")); + _logger.Error(GetType(), ex, "Error saving relation type with {Id}", relationType.Id); + throw HttpResponseException.CreateNotificationValidationErrorResponse("Something went wrong when saving the relation type"); } } @@ -164,16 +163,16 @@ namespace Umbraco.Web.Editors /// A . [HttpPost] [HttpDelete] - public HttpResponseMessage DeleteById(int id) + public IActionResult DeleteById(int id) { - var relationType = Services.RelationService.GetRelationTypeById(id); + var relationType = _relationService.GetRelationTypeById(id); - if(relationType == null) - throw new HttpResponseException(HttpStatusCode.NotFound); + if (relationType == null) + return NotFound(); - Services.RelationService.Delete(relationType); + _relationService.Delete(relationType); - return Request.CreateResponse(HttpStatusCode.OK); + return Ok(); } } } diff --git a/src/Umbraco.Web/Editors/StylesheetController.cs b/src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs similarity index 66% rename from src/Umbraco.Web/Editors/StylesheetController.cs rename to src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs index 9785644d3f..852bff28c1 100644 --- a/src/Umbraco.Web/Editors/StylesheetController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Services; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Editors; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -namespace Umbraco.Web.Editors +namespace Umbraco.Web.BackOffice.Controllers { /// /// The API controller used for retrieving available stylesheets @@ -12,9 +14,16 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] public class StylesheetController : UmbracoAuthorizedJsonController { + private readonly IFileService _fileService; + + public StylesheetController(IFileService fileService) + { + _fileService = fileService; + } + public IEnumerable GetAll() { - return Services.FileService.GetStylesheets() + return _fileService.GetStylesheets() .Select(x => new Stylesheet() { Name = x.Alias, @@ -24,7 +33,7 @@ namespace Umbraco.Web.Editors public IEnumerable GetRulesByName(string name) { - var css = Services.FileService.GetStylesheetByName(name.EnsureEndsWith(".css")); + var css = _fileService.GetStylesheetByName(name.EnsureEndsWith(".css")); if (css == null) return Enumerable.Empty(); @@ -32,4 +41,4 @@ namespace Umbraco.Web.Editors } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Editors/TemplateController.cs b/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs similarity index 67% rename from src/Umbraco.Web/Editors/TemplateController.cs rename to src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs index a02536e2bb..6eae71a27e 100644 --- a/src/Umbraco.Web/Editors/TemplateController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs @@ -2,43 +2,36 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using System.Net.Http; -using System.Web.Http; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; +using Microsoft.AspNetCore.Mvc; using Umbraco.Core.IO; -using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Models; -using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Core.Strings; +using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Umbraco.Web.Routing; -using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Editors { [PluginController("UmbracoApi")] - [UmbracoTreeAuthorize(Constants.Trees.Templates)] + [UmbracoTreeAuthorizeAttribute(Constants.Trees.Templates)] public class TemplateController : BackOfficeNotificationsController { + private readonly IFileService _fileService; + private readonly UmbracoMapper _umbracoMapper; + private readonly IShortStringHelper _shortStringHelper; + public TemplateController( - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger logger, - IRuntimeState runtimeState, - IShortStringHelper shortStringHelper, + IFileService fileService, UmbracoMapper umbracoMapper, - IPublishedUrlProvider publishedUrlProvider) - : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) + IShortStringHelper shortStringHelper) { + _fileService = fileService ?? throw new ArgumentNullException(nameof(fileService)); + _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); } /// @@ -48,8 +41,8 @@ namespace Umbraco.Web.Editors /// public TemplateDisplay GetByAlias(string alias) { - var template = Services.FileService.GetTemplate(alias); - return template == null ? null : Mapper.Map(template); + var template = _fileService.GetTemplate(alias); + return template == null ? null : _umbracoMapper.Map(template); } /// @@ -58,7 +51,7 @@ namespace Umbraco.Web.Editors /// public IEnumerable GetAll() { - return Services.FileService.GetTemplates().Select(Mapper.Map); + return _fileService.GetTemplates().Select(_umbracoMapper.Map); } /// @@ -68,11 +61,11 @@ namespace Umbraco.Web.Editors /// public TemplateDisplay GetById(int id) { - var template = Services.FileService.GetTemplate(id); + var template = _fileService.GetTemplate(id); if (template == null) throw new HttpResponseException(HttpStatusCode.NotFound); - return Mapper.Map(template); + return _umbracoMapper.Map(template); } /// @@ -82,25 +75,25 @@ namespace Umbraco.Web.Editors /// [HttpDelete] [HttpPost] - public HttpResponseMessage DeleteById(int id) + public IActionResult DeleteById(int id) { - var template = Services.FileService.GetTemplate(id); + var template = _fileService.GetTemplate(id); if (template == null) throw new HttpResponseException(HttpStatusCode.NotFound); - Services.FileService.DeleteTemplate(template.Alias); - return Request.CreateResponse(HttpStatusCode.OK); + _fileService.DeleteTemplate(template.Alias); + return Ok(); } public TemplateDisplay GetScaffold(int id) { //empty default - var dt = new Template(ShortStringHelper, string.Empty, string.Empty); + var dt = new Template(_shortStringHelper, string.Empty, string.Empty); dt.Path = "-1"; if (id > 0) { - var master = Services.FileService.GetTemplate(id); + var master = _fileService.GetTemplate(id); if(master != null) { dt.SetMasterTemplate(master); @@ -108,7 +101,7 @@ namespace Umbraco.Web.Editors } var content = ViewHelper.GetDefaultFileContent( layoutPageAlias: dt.MasterTemplateAlias ); - var scaffold = Mapper.Map(dt); + var scaffold = _umbracoMapper.Map(dt); scaffold.Content = content + "\r\n\r\n@* the fun starts here *@\r\n\r\n"; return scaffold; @@ -119,33 +112,33 @@ namespace Umbraco.Web.Editors /// /// /// - public TemplateDisplay PostSave(TemplateDisplay display) + public ActionResult PostSave(TemplateDisplay display) { //Checking the submitted is valid with the Required attributes decorated on the ViewModel if (ModelState.IsValid == false) { - throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + return ValidationProblem(ModelState); } if (display.Id > 0) { // update - var template = Services.FileService.GetTemplate(display.Id); + var template = _fileService.GetTemplate(display.Id); if (template == null) throw new HttpResponseException(HttpStatusCode.NotFound); var changeMaster = template.MasterTemplateAlias != display.MasterTemplateAlias; var changeAlias = template.Alias != display.Alias; - Mapper.Map(display, template); + _umbracoMapper.Map(display, template); if (changeMaster) { if (string.IsNullOrEmpty(display.MasterTemplateAlias) == false) { - var master = Services.FileService.GetTemplate(display.MasterTemplateAlias); + var master = _fileService.GetTemplate(display.MasterTemplateAlias); if(master == null || master.Id == display.Id) { template.SetMasterTemplate(null); @@ -154,7 +147,7 @@ namespace Umbraco.Web.Editors template.SetMasterTemplate(master); //After updating the master - ensure we update the path property if it has any children already assigned - var templateHasChildren = Services.FileService.GetTemplateDescendants(display.Id); + var templateHasChildren = _fileService.GetTemplateDescendants(display.Id); foreach (var childTemplate in templateHasChildren) { @@ -177,7 +170,7 @@ namespace Umbraco.Web.Editors childTemplate.Path = master.Path + "," + display.Id + "," + childTemplatePath; //Save the children with the updated path - Services.FileService.SaveTemplate(childTemplate); + _fileService.SaveTemplate(childTemplate); } } } @@ -188,14 +181,14 @@ namespace Umbraco.Web.Editors } } - Services.FileService.SaveTemplate(template); + _fileService.SaveTemplate(template); if (changeAlias) { - template = Services.FileService.GetTemplate(template.Id); + template = _fileService.GetTemplate(template.Id); } - Mapper.Map(template, display); + _umbracoMapper.Map(template, display); } else { @@ -203,15 +196,15 @@ namespace Umbraco.Web.Editors ITemplate master = null; if (string.IsNullOrEmpty(display.MasterTemplateAlias) == false) { - master = Services.FileService.GetTemplate(display.MasterTemplateAlias); + master = _fileService.GetTemplate(display.MasterTemplateAlias); if (master == null) throw new HttpResponseException(HttpStatusCode.NotFound); } // we need to pass the template name as alias to keep the template file casing consistent with templates created with content // - see comment in FileService.CreateTemplateForContentType for additional details - var template = Services.FileService.CreateTemplateWithIdentity(display.Name, display.Name, display.Content, master); - Mapper.Map(template, display); + var template = _fileService.CreateTemplateWithIdentity(display.Name, display.Name, display.Content, master); + _umbracoMapper.Map(template, display); } return display; diff --git a/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs new file mode 100644 index 0000000000..271ba17e74 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs @@ -0,0 +1,217 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Core; + +namespace Umbraco.Extensions +{ + internal static class ModelStateExtensions + { + + /// + /// Checks if there are any model errors on any fields containing the prefix + /// + /// + /// + /// + public static bool IsValid(this ModelStateDictionary state, string prefix) + { + return state.Where(v => v.Key.StartsWith(prefix + ".")).All(v => !v.Value.Errors.Any()); + } + + /// + /// Adds the error to model state correctly for a property so we can use it on the client side. + /// + /// + /// + /// + /// The culture for the property, if the property is invariant than this is empty + internal static void AddPropertyError(this ModelStateDictionary modelState, + ValidationResult result, string propertyAlias, string culture = "", string segment = "") + { + if (culture == null) + culture = ""; + modelState.AddValidationError(result, "_Properties", propertyAlias, + //if the culture is null, we'll add the term 'invariant' as part of the key + culture.IsNullOrWhiteSpace() ? "invariant" : culture, + // if the segment is null, we'll add the term 'null' as part of the key + segment.IsNullOrWhiteSpace() ? "null" : segment); + } + + /// + /// Adds a generic culture error for use in displaying the culture validation error in the save/publish/etc... dialogs + /// + /// + /// + /// + /// + internal static void AddVariantValidationError(this ModelStateDictionary modelState, + string culture, string segment, string errMsg) + { + var key = "_content_variant_" + (culture.IsNullOrWhiteSpace() ? "invariant" : culture) + "_" + (segment.IsNullOrWhiteSpace() ? "null" : segment) + "_"; + if (modelState.ContainsKey(key)) return; + modelState.AddModelError(key, errMsg); + } + + /// + /// Returns a list of cultures that have property validation errors + /// + /// + /// + /// The culture to affiliate invariant errors with + /// + /// A list of cultures that have property validation errors. The default culture will be returned for any invariant property errors. + /// + internal static IReadOnlyList<(string culture, string segment)> GetVariantsWithPropertyErrors(this ModelStateDictionary modelState, + string cultureForInvariantErrors) + { + //Add any variant specific errors here + var variantErrors = modelState.Keys + .Where(key => key.StartsWith("_Properties.")) //only choose _Properties errors + .Select(x => x.Split('.')) //split into parts + .Where(x => x.Length >= 4 && !x[2].IsNullOrWhiteSpace() && !x[3].IsNullOrWhiteSpace()) + .Select(x => (culture: x[2], segment: x[3])) + //if the culture is marked "invariant" than return the default language, this is because we can only edit invariant properties on the default language + //so errors for those must show up under the default lang. + //if the segment is marked "null" then return an actual null + .Select(x => + { + var culture = x.culture == "invariant" ? cultureForInvariantErrors : x.culture; + var segment = x.segment == "null" ? null : x.segment; + return (culture, segment); + }) + .Distinct() + .ToList(); + + return variantErrors; + } + + /// + /// Returns a list of cultures that have any validation errors + /// + /// + /// + /// The culture to affiliate invariant errors with + /// + /// A list of cultures that have validation errors. The default culture will be returned for any invariant errors. + /// + internal static IReadOnlyList<(string culture, string segment)> GetVariantsWithErrors(this ModelStateDictionary modelState, string cultureForInvariantErrors) + { + var propertyVariantErrors = modelState.GetVariantsWithPropertyErrors(cultureForInvariantErrors); + + //now check the other special variant errors that are + var genericVariantErrors = modelState.Keys + .Where(x => x.StartsWith("_content_variant_") && x.EndsWith("_")) + .Select(x => x.TrimStart("_content_variant_").TrimEnd("_")) + .Select(x => + { + // Format "_" + var cs = x.Split(new[] { '_' }); + return (culture: cs[0], segment: cs[1]); + }) + .Where(x => !x.culture.IsNullOrWhiteSpace()) + //if it's marked "invariant" than return the default language, this is because we can only edit invariant properties on the default language + //so errors for those must show up under the default lang. + //if the segment is marked "null" then return an actual null + .Select(x => + { + var culture = x.culture == "invariant" ? cultureForInvariantErrors : x.culture; + var segment = x.segment == "null" ? null : x.segment; + return (culture, segment); + }) + .Distinct(); + + return propertyVariantErrors.Union(genericVariantErrors).Distinct().ToList(); + } + + /// + /// Adds the error to model state correctly for a property so we can use it on the client side. + /// + /// + /// + /// + /// Each model state validation error has a name and in most cases this name is made up of parts which are delimited by a '.' + /// + internal static void AddValidationError(this ModelStateDictionary modelState, + ValidationResult result, params string[] parts) + { + // if there are assigned member names, we combine the member name with the owner name + // so that we can try to match it up to a real field. otherwise, we assume that the + // validation message is for the overall owner. + // Owner = the component being validated, like a content property but could be just an HTML field on another editor + + var withNames = false; + var delimitedParts = string.Join(".", parts); + foreach (var memberName in result.MemberNames) + { + modelState.TryAddModelError($"{delimitedParts}.{memberName}", result.ErrorMessage); + withNames = true; + } + if (!withNames) + { + modelState.TryAddModelError($"{delimitedParts}", result.ErrorMessage); + } + + } + + /// + /// Will add an error to model state for a key if that key and error don't already exist + /// + /// + /// + /// + /// + private static bool TryAddModelError(this ModelStateDictionary modelState, string key, string errorMsg) + { + if (modelState.TryGetValue(key, out var errs)) + { + foreach(var e in errs.Errors) + if (e.ErrorMessage == errorMsg) return false; //if this same error message exists for the same key, just exit + } + + modelState.AddModelError(key, errorMsg); + return true; + } + + public static IDictionary ToErrorDictionary(this ModelStateDictionary modelState) + { + var modelStateError = new Dictionary(); + foreach (var keyModelStatePair in modelState) + { + var key = keyModelStatePair.Key; + var errors = keyModelStatePair.Value.Errors; + if (errors != null && errors.Count > 0) + { + modelStateError.Add(key, errors.Select(error => error.ErrorMessage)); + } + } + return modelStateError; + } + + /// + /// Serializes the ModelState to JSON for JavaScript to interrogate the errors + /// + /// + /// + public static JsonResult ToJsonErrors(this ModelStateDictionary state) + { + return new JsonResult(new + { + success = state.IsValid.ToString().ToLower(), + failureType = "ValidationError", + validationErrors = from e in state + where e.Value.Errors.Count > 0 + select new + { + name = e.Key, + errors = e.Value.Errors.Select(x => x.ErrorMessage) + .Concat( + e.Value.Errors.Where(x => x.Exception != null).Select(x => x.Exception.Message)) + } + }); + } + + } +} diff --git a/src/Umbraco.Web/Editors/Filters/DataTypeValidateAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/DataTypeValidateAttribute.cs similarity index 60% rename from src/Umbraco.Web/Editors/Filters/DataTypeValidateAttribute.cs rename to src/Umbraco.Web.BackOffice/Filters/DataTypeValidateAttribute.cs index efdcf93fff..41e928053d 100644 --- a/src/Umbraco.Web/Editors/Filters/DataTypeValidateAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/DataTypeValidateAttribute.cs @@ -1,16 +1,16 @@ using System; using System.Linq; using System.Net; -using System.Net.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; +using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Core; -using Umbraco.Web.Composing; +using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; +using Umbraco.Extensions; +using Umbraco.Web.Common.ActionsResults; +using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.WebApi; namespace Umbraco.Web.Editors { @@ -19,38 +19,36 @@ namespace Umbraco.Web.Editors /// internal sealed class DataTypeValidateAttribute : ActionFilterAttribute { - public IDataTypeService DataTypeService { get; } + private readonly IDataTypeService _dataTypeService; + private readonly PropertyEditorCollection _propertyEditorCollection; + private readonly UmbracoMapper _umbracoMapper; - public PropertyEditorCollection PropertyEditors { get; } - - public DataTypeValidateAttribute() - : this(Current.Factory.GetInstance(), Current.Factory.GetInstance()) - { - } /// /// For use in unit tests. Not possible to use as attribute ctor. /// /// - /// - public DataTypeValidateAttribute(IDataTypeService dataTypeService, PropertyEditorCollection propertyEditors) + /// + /// + public DataTypeValidateAttribute(IDataTypeService dataTypeService, PropertyEditorCollection propertyEditorCollection, UmbracoMapper umbracoMapper) { - DataTypeService = dataTypeService; - PropertyEditors = propertyEditors; + _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); + _propertyEditorCollection = propertyEditorCollection ?? throw new ArgumentNullException(nameof(propertyEditorCollection)); + _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); } - public override void OnActionExecuting(HttpActionContext actionContext) + public override void OnActionExecuting(ActionExecutingContext context) { - var dataType = (DataTypeSave) actionContext.ActionArguments["dataType"]; + var dataType = (DataTypeSave) context.ActionArguments["dataType"]; dataType.Name = dataType.Name.CleanForXss('[', ']', '(', ')', ':'); dataType.Alias = dataType.Alias == null ? dataType.Name : dataType.Alias.CleanForXss('[', ']', '(', ')', ':'); // get the property editor, ensuring that it exits - if (!PropertyEditors.TryGet(dataType.EditorAlias, out var propertyEditor)) + if (!_propertyEditorCollection.TryGet(dataType.EditorAlias, out var propertyEditor)) { var message = $"Property editor \"{dataType.EditorAlias}\" was not found."; - actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message); + context.Result = new UmbracoProblemResult(message, HttpStatusCode.NotFound); return; } @@ -62,25 +60,25 @@ namespace Umbraco.Web.Editors switch (dataType.Action) { case ContentSaveAction.Save: - persisted = DataTypeService.GetDataType(Convert.ToInt32(dataType.Id)); + persisted = _dataTypeService.GetDataType(Convert.ToInt32(dataType.Id)); if (persisted == null) { var message = $"Data type with id {dataType.Id} was not found."; - actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message); + context.Result = new UmbracoProblemResult(message, HttpStatusCode.NotFound); return; } // map the model to the persisted instance - Current.Mapper.Map(dataType, persisted); + _umbracoMapper.Map(dataType, persisted); break; case ContentSaveAction.SaveNew: // create the persisted model from mapping the saved model - persisted = Current.Mapper.Map(dataType); + persisted = _umbracoMapper.Map(dataType); ((DataType) persisted).ResetIdentity(); break; default: - actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, new ArgumentOutOfRangeException()); + context.Result = new UmbracoProblemResult($"Data type action {dataType.Action} was not found.", HttpStatusCode.NotFound); return; } @@ -98,13 +96,13 @@ namespace Umbraco.Web.Editors // run each IValueValidator (with null valueType and dataTypeConfiguration: not relevant here) foreach (var validator in editorField.Validators) foreach (var result in validator.Validate(field.Value, null, null)) - actionContext.ModelState.AddValidationError(result, "Properties", field.Key); + context.ModelState.AddValidationError(result, "Properties", field.Key); } - if (actionContext.ModelState.IsValid == false) + if (context.ModelState.IsValid == false) { // if it is not valid, do not continue and return the model state - actionContext.Response = actionContext.Request.CreateValidationErrorResponse(actionContext.ModelState); + throw HttpResponseException.CreateValidationErrorResponse(context.ModelState); } } } diff --git a/src/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttribute.cs index 9ee289a39c..598aa35219 100644 --- a/src/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttribute.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.DependencyInjection; using Umbraco.Web.Common.Extensions; namespace Umbraco.Web.BackOffice.Filters diff --git a/src/Umbraco.Web.BackOffice/Filters/UmbracoApplicationAuthorizeAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/UmbracoApplicationAuthorizeAttribute.cs index 88a4c4f8ff..81e61af5bf 100644 --- a/src/Umbraco.Web.BackOffice/Filters/UmbracoApplicationAuthorizeAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/UmbracoApplicationAuthorizeAttribute.cs @@ -7,49 +7,62 @@ using Umbraco.Core; namespace Umbraco.Web.BackOffice.Filters { - public class UmbracoApplicationAuthorizeAttribute : Attribute, IAuthorizationFilter + public class UmbracoApplicationAuthorizeAttribute : TypeFilterAttribute { - /// - /// Can be used by unit tests to enable/disable this filter - /// - internal static bool Enable = true; - - private readonly string[] _appNames; - - /// - /// Constructor to set any number of applications that the user needs access to be authorized - /// - /// - /// If the user has access to any of the specified apps, they will be authorized. - /// - public UmbracoApplicationAuthorizeAttribute(params string[] appName) + public UmbracoApplicationAuthorizeAttribute(params string[] appName) : base(typeof(UmbracoApplicationAuthorizeFilter)) { - _appNames = appName; + base.Arguments = new object[] + { + appName + }; } - - public void OnAuthorization(AuthorizationFilterContext context) + private class UmbracoApplicationAuthorizeFilter : IAuthorizationFilter { - var umbracoContextAccessor = context.HttpContext.RequestServices.GetRequiredService(); - if (!IsAuthorized(umbracoContextAccessor)) - { - context.Result = new ForbidResult(); - } - } + /// + /// Can be used by unit tests to enable/disable this filter + /// + internal static bool Enable = true; - private bool IsAuthorized(IUmbracoContextAccessor umbracoContextAccessor) - { - if (Enable == false) + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly string[] _appNames; + + /// + /// Constructor to set any number of applications that the user needs access to be authorized + /// + /// + /// If the user has access to any of the specified apps, they will be authorized. + /// + public UmbracoApplicationAuthorizeFilter(IUmbracoContextAccessor umbracoContextAccessor, params string[] appName) { - return true; + _umbracoContextAccessor = umbracoContextAccessor; + _appNames = appName; } - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - var authorized = umbracoContext.Security.CurrentUser != null - && _appNames.Any(app => umbracoContext.Security.UserHasSectionAccess( - app, umbracoContext.Security.CurrentUser)); - return authorized; + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!IsAuthorized()) + { + context.Result = new ForbidResult(); + } + } + + private bool IsAuthorized() + { + if (Enable == false) + { + return true; + } + + var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + var authorized = umbracoContext.Security.CurrentUser != null + && _appNames.Any(app => umbracoContext.Security.UserHasSectionAccess( + app, umbracoContext.Security.CurrentUser)); + + return authorized; + } } } + } diff --git a/src/Umbraco.Web.BackOffice/Filters/UmbracoTreeAuthorizeAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/UmbracoTreeAuthorizeAttribute.cs new file mode 100644 index 0000000000..6db37d16f6 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Filters/UmbracoTreeAuthorizeAttribute.cs @@ -0,0 +1,85 @@ +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Core; +using Umbraco.Web.Services; + +namespace Umbraco.Web.BackOffice.Filters +{ + + public class UmbracoTreeAuthorizeAttribute : TypeFilterAttribute + { + public UmbracoTreeAuthorizeAttribute(params string[] treeAliases) : base(typeof(UmbracoTreeAuthorizeFilter)) + { + base.Arguments = new object[] + { + treeAliases + }; + } + + /// + /// Ensures that the current user has access to the application for which the specified tree(s) belongs + /// + /// + /// This would allow a tree to be moved between sections + /// + private sealed class UmbracoTreeAuthorizeFilter : IAuthorizationFilter + { + /// + /// Can be used by unit tests to enable/disable this filter + /// + internal static bool Enable = true; + + private readonly ITreeService _treeService; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly string[] _treeAliases; + + /// + /// Constructor to set authorization to be based on a tree alias for which application security will be applied + /// + /// + /// + /// If the user has access to the application that the treeAlias is specified in, they will be authorized. + /// Multiple trees may be specified. + /// + /// + public UmbracoTreeAuthorizeFilter(ITreeService treeService, IUmbracoContextAccessor umbracoContextAccessor, params string[] treeAliases) + { + _treeService = treeService; + _umbracoContextAccessor = umbracoContextAccessor; + _treeAliases = treeAliases; + } + + private bool IsAuthorized() + { + if (Enable == false) + { + return true; + } + + var apps = _treeAliases.Select(x => _treeService + .GetByAlias(x)) + .WhereNotNull() + .Select(x => x.SectionAlias) + .Distinct() + .ToArray(); + + var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + return umbracoContext.Security.CurrentUser != null + && apps.Any(app => umbracoContext.Security.UserHasSectionAccess( + app, umbracoContext.Security.CurrentUser)); + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!IsAuthorized()) + { + + context.Result = new ForbidResult(); + } + } + } + } + + +} diff --git a/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs index d2e22dc623..b8f035077b 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs @@ -109,7 +109,6 @@ namespace Umbraco.Web.BackOffice.Filters _logger.Error(ex, "Could not validate XSRF token"); return false; } - return true; } } } diff --git a/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs b/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs index db2b72f989..131a4ac62c 100644 --- a/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs +++ b/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Web.Editors; using Umbraco.Core.Configuration.HealthChecks; @@ -12,7 +13,7 @@ namespace Umbraco.Web.HealthCheck /// /// The API controller used to display the health check info and execute any actions /// - [UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)] + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Settings)] public class HealthCheckController : UmbracoAuthorizedJsonController { private readonly HealthCheckCollection _checks; diff --git a/src/Umbraco.Web.BackOffice/Profiling/WebProfilingController.cs b/src/Umbraco.Web.BackOffice/Profiling/WebProfilingController.cs index a053c28d42..b6cdf75f6f 100644 --- a/src/Umbraco.Web.BackOffice/Profiling/WebProfilingController.cs +++ b/src/Umbraco.Web.BackOffice/Profiling/WebProfilingController.cs @@ -1,4 +1,6 @@ -using Umbraco.Core.Hosting; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; +using Umbraco.Core.Hosting; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Editors; @@ -8,8 +10,8 @@ namespace Umbraco.Web.BackOffice.Profiling /// /// The API controller used to display the state of the web profiler /// - [UmbracoApplicationAuthorizeAttribute(Core.Constants.Applications.Settings)] - public class WebProfilingController : UmbracoAuthorizedJsonController + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Settings)] + public class WebProfilingController : UmbracoAuthorizedJsonController { private readonly IHostingEnvironment _hosting; diff --git a/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs b/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs new file mode 100644 index 0000000000..3c94e3f9a0 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs @@ -0,0 +1,42 @@ +using System; +using System.Linq; +using System.Text; +using System.Web; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; + +namespace Umbraco.Extensions +{ + public static class UrlHelperExtensions + { + internal static string GetTreePathFromFilePath(this IUrlHelper urlHelper, string virtualPath, string basePath = "") + { + //This reuses the Logic from umbraco.cms.helpers.DeepLink class + //to convert a filepath to a tree syncing path string. + + //removes the basepath from the path + //and normalizes paths - / is used consistently between trees and editors + basePath = basePath.TrimStart("~"); + virtualPath = virtualPath.TrimStart("~"); + virtualPath = virtualPath.Substring(basePath.Length); + virtualPath = virtualPath.Replace('\\', '/'); + + //-1 is the default root id for trees + var sb = new StringBuilder("-1"); + + //split the virtual path and iterate through it + var pathPaths = virtualPath.Split('/'); + + for (var p = 0; p < pathPaths.Length; p++) + { + var path = HttpUtility.UrlEncode(string.Join("/", pathPaths.Take(p + 1))); + if (string.IsNullOrEmpty(path) == false) + { + sb.Append(","); + sb.Append(path); + } + } + return sb.ToString().TrimEnd(","); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Validation/PrefixlessBodyModelValidator.cs b/src/Umbraco.Web.BackOffice/Validation/PrefixlessBodyModelValidator.cs new file mode 100644 index 0000000000..d22b044e51 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Validation/PrefixlessBodyModelValidator.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; + +namespace Umbraco.Web.BackOffice.Validation +{ + public class PrefixlessBodyModelValidator : ObjectModelValidator + { + public PrefixlessBodyModelValidator(IModelMetadataProvider modelMetadataProvider, IList validatorProviders) : + base(modelMetadataProvider, validatorProviders) + { + + } + + public override ValidationVisitor GetValidationVisitor(ActionContext actionContext, IModelValidatorProvider validatorProvider, + ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary validationState) + { + var visitor = new PrefixlessValidationVisitor( + actionContext, + validatorProvider, + validatorCache, + metadataProvider, + validationState); + + return visitor; + } + + private class PrefixlessValidationVisitor : ValidationVisitor + { + public PrefixlessValidationVisitor(ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary validationState) + : base(actionContext, validatorProvider, validatorCache, metadataProvider, validationState) { + + } + + public override bool Validate(ModelMetadata metadata, string key, object model, bool alwaysValidateAtTopLevel) + { + return base.Validate(metadata, string.Empty, model, alwaysValidateAtTopLevel); + } + } + } + + +} diff --git a/src/Umbraco.Web.Common/ActionsResults/UmbracoProblemResult.cs b/src/Umbraco.Web.Common/ActionsResults/UmbracoProblemResult.cs new file mode 100644 index 0000000000..235ef0c037 --- /dev/null +++ b/src/Umbraco.Web.Common/ActionsResults/UmbracoProblemResult.cs @@ -0,0 +1,13 @@ +using System.Net; +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Web.Common.ActionsResults +{ + public class UmbracoProblemResult : ObjectResult + { + public UmbracoProblemResult(string message, HttpStatusCode httpStatusCode = HttpStatusCode.InternalServerError) : base(new {Message = message}) + { + StatusCode = (int) httpStatusCode; + } + } +} diff --git a/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs b/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs index d5268a884f..bbd9d5207a 100644 --- a/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs +++ b/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs @@ -35,7 +35,7 @@ namespace Umbraco.Web.Common.ApplicationModels ActionModelConventions = new List() { - new ClientErrorResultFilterConvention(), // TODO: Need to determine exactly how this affects errors + new ClientErrorResultFilterConvention(), // Ensures the responses without any body is converted into a simple json object with info instead of a string like "Status Code: 404; Not Found" new InvalidModelStateFilterConvention(), // automatically 400 responses if ModelState is invalid before hitting the controller new ConsumesConstraintForFormFileParameterConvention(), // If an controller accepts files, it must accept multipart/form-data. new InferParameterBindingInfoConvention(modelMetadataProvider), // no need for [FromBody] everywhere, A complex type parameter is assigned to FromBody @@ -69,14 +69,16 @@ namespace Umbraco.Web.Common.ApplicationModels if (!IsUmbracoApiController(controller)) continue; + + foreach (var action in controller.Actions) { foreach (var convention in ActionModelConventions) { convention.Apply(action); - } + } } - + } } diff --git a/src/Umbraco.Web.Common/Exceptions/HttpResponseException.cs b/src/Umbraco.Web.Common/Exceptions/HttpResponseException.cs index 750417fab6..cb14a5a546 100644 --- a/src/Umbraco.Web.Common/Exceptions/HttpResponseException.cs +++ b/src/Umbraco.Web.Common/Exceptions/HttpResponseException.cs @@ -4,6 +4,7 @@ using System.Net; using System.Runtime.Serialization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; +using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Common.Exceptions { @@ -68,5 +69,16 @@ namespace Umbraco.Web.Common.Exceptions } }; } + + public static HttpResponseException CreateNotificationValidationErrorResponse(string errorMessage) + { + var notificationModel = new SimpleNotificationModel + { + Message = errorMessage + }; + notificationModel.AddErrorNotification(errorMessage, string.Empty); + return CreateValidationErrorResponse(notificationModel); + } + } } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 5078fa5f22..eb14267ecd 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -248,7 +248,7 @@ namespace Umbraco.Extensions var connStrings = configs.ConnectionStrings(); var appSettingMainDomLock = globalSettings.MainDomLock; var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" - ? (IMainDomLock)new SqlMainDomLock(logger, globalSettings, connStrings, dbProviderFactoryCreator) + ? (IMainDomLock)new SqlMainDomLock(logger, globalSettings, connStrings, dbProviderFactoryCreator, hostingEnvironment) : new MainDomSemaphoreLock(logger, hostingEnvironment); var mainDom = new MainDom(logger, mainDomLock); diff --git a/src/Umbraco.Web.Common/Formatters/AngularJsonMediaTypeFormatter.cs b/src/Umbraco.Web.Common/Formatters/AngularJsonMediaTypeFormatter.cs index 9a10269398..5e400c78eb 100644 --- a/src/Umbraco.Web.Common/Formatters/AngularJsonMediaTypeFormatter.cs +++ b/src/Umbraco.Web.Common/Formatters/AngularJsonMediaTypeFormatter.cs @@ -3,6 +3,7 @@ using System.IO; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Formatters; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace Umbraco.Web.Common.Formatters { @@ -19,6 +20,7 @@ namespace Umbraco.Web.Common.Formatters public AngularJsonMediaTypeFormatter(JsonSerializerSettings serializerSettings, ArrayPool charPool, MvcOptions mvcOptions) : base(serializerSettings, charPool, mvcOptions) { + serializerSettings.Converters.Add(new VersionConverter()); } protected override JsonWriter CreateJsonWriter(TextWriter writer) diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index babf6cb80d..5ea29cef1d 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -21,6 +21,8 @@ using Umbraco.Web.Common.Controllers; using System; using Umbraco.Web.Common.Middleware; using Umbraco.Web.Common.ModelBinding; +using Umbraco.Web.Search; +using Umbraco.Web.Trees; namespace Umbraco.Web.Common.Runtime { @@ -79,6 +81,14 @@ namespace Umbraco.Web.Common.Runtime composition.WithCollectionBuilder() .Add(umbracoApiControllerTypes); + // register back office trees + // the collection builder only accepts types inheriting from TreeControllerBase + // and will filter out those that are not attributed with TreeAttribute + // composition.Trees() + // .AddTreeControllers(umbracoApiControllerTypes.Where(x => typeof(TreeControllerBase).IsAssignableFrom(x))); + composition.RegisterUnique(); //TODO replace with collection builder above + + composition.RegisterUnique(); composition.RegisterUnique(); @@ -86,6 +96,10 @@ namespace Umbraco.Web.Common.Runtime composition.RegisterUnique(); composition.RegisterUnique(); + + + + } } } 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 new file mode 100644 index 0000000000..90eae85800 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_US.js @@ -0,0 +1,261 @@ +tinymce.addI18n('en_US',{ +"Redo": "Redo", +"Undo": "Undo", +"Cut": "Cut", +"Copy": "Copy", +"Paste": "Paste", +"Select all": "Select all", +"New document": "New document", +"Ok": "Ok", +"Cancel": "Cancel", +"Visual aids": "Visual aids", +"Bold": "Bold", +"Italic": "Italic", +"Underline": "Underline", +"Strikethrough": "Strikethrough", +"Superscript": "Superscript", +"Subscript": "Subscript", +"Clear formatting": "Clear formatting", +"Align left": "Align left", +"Align center": "Align center", +"Align right": "Align right", +"Justify": "Justify", +"Bullet list": "Bullet list", +"Numbered list": "Numbered list", +"Decrease indent": "Decrease indent", +"Increase indent": "Increase indent", +"Close": "Close", +"Formats": "Formats", +"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.", +"Headers": "Headers", +"Header 1": "Header 1", +"Header 2": "Header 2", +"Header 3": "Header 3", +"Header 4": "Header 4", +"Header 5": "Header 5", +"Header 6": "Header 6", +"Headings": "Headings", +"Heading 1": "Heading 1", +"Heading 2": "Heading 2", +"Heading 3": "Heading 3", +"Heading 4": "Heading 4", +"Heading 5": "Heading 5", +"Heading 6": "Heading 6", +"Preformatted": "Preformatted", +"Div": "Div", +"Pre": "Pre", +"Code": "Code", +"Paragraph": "Paragraph", +"Blockquote": "Blockquote", +"Inline": "Inline", +"Blocks": "Blocks", +"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.", +"Font Family": "Font Family", +"Font Sizes": "Font Sizes", +"Class": "Class", +"Browse for an image": "Browse for an image", +"OR": "OR", +"Drop an image here": "Drop an image here", +"Upload": "Upload", +"Block": "Blocks", +"Align": "Align", +"Default": "Default", +"Circle": "Circle", +"Disc": "Disc", +"Square": "Square", +"Lower Alpha": "Lower Alpha", +"Lower Greek": "Lower Greek", +"Lower Roman": "Lower Roman", +"Upper Alpha": "Upper Alpha", +"Upper Roman": "Upper Roman", +"Anchor": "Anchor", +"Name": "Name", +"Id": "ID", +"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "ID should start with a letter, followed only by letters, numbers, dashes, dots, colons, or underscores.", +"You have unsaved changes are you sure you want to navigate away?": "You have unsaved changes are you sure you want to navigate away?", +"Restore last draft": "Restore last draft", +"Special character": "Special character", +"Source code": "Source code", +"Insert\/Edit code sample": "Insert\/Edit code sample", +"Language": "Language", +"Code sample": "Code sample", +"Color": "color", +"R": "R", +"G": "G", +"B": "B", +"Left to right": "Left to right", +"Right to left": "Right to left", +"Emoticons": "Emoticons", +"Document properties": "Document properties", +"Title": "Title", +"Keywords": "Keywords", +"Description": "Description", +"Robots": "Robots", +"Author": "Author", +"Encoding": "Encoding", +"Fullscreen": "Fullscreen", +"Action": "Action", +"Shortcut": "Shortcut", +"Help": "Help", +"Address": "Address", +"Focus to menubar": "Focus to menubar", +"Focus to toolbar": "Focus to toolbar", +"Focus to element path": "Focus to element path", +"Focus to contextual toolbar": "Focus to contextual toolbar", +"Insert link (if link plugin activated)": "Insert link (if link plugin activated)", +"Save (if save plugin activated)": "Save (if save plugin activated)", +"Find (if searchreplace plugin activated)": "Find (if searchreplace plugin activated)", +"Plugins installed ({0}):": "Plugins installed ({0}):", +"Premium plugins:": "Premium plugins:", +"Learn more...": "Learn more...", +"You are using {0}": "You are using {0}", +"Plugins": "Plugins", +"Handy Shortcuts": "Handy Shortcuts", +"Horizontal line": "Horizontal line", +"Insert\/edit image": "Insert\/edit image", +"Image description": "Image description", +"Source": "Source", +"Dimensions": "Dimensions", +"Constrain proportions": "Constrain proportions", +"General": "General", +"Advanced": "Advanced", +"Style": "Style", +"Vertical space": "Vertical space", +"Horizontal space": "Horizontal space", +"Border": "Border", +"Insert image": "Insert image", +"Image": "Image", +"Image list": "Image list", +"Rotate counterclockwise": "Rotate counterclockwise", +"Rotate clockwise": "Rotate clockwise", +"Flip vertically": "Flip vertically", +"Flip horizontally": "Flip horizontally", +"Edit image": "Edit image", +"Image options": "Image options", +"Zoom in": "Zoom in", +"Zoom out": "Zoom out", +"Crop": "Crop", +"Resize": "Resize", +"Orientation": "Orientation", +"Brightness": "Brightness", +"Sharpen": "Sharpen", +"Contrast": "Contrast", +"Color levels": "color levels", +"Gamma": "Gamma", +"Invert": "Invert", +"Apply": "Apply", +"Back": "Back", +"Insert date\/time": "Insert date\/time", +"Date\/time": "Date\/time", +"Insert link": "Insert link", +"Insert\/edit link": "Insert\/edit link", +"Text to display": "Text to display", +"Url": "Url", +"Target": "Target", +"None": "None", +"New window": "New window", +"Remove link": "Remove link", +"Anchors": "Anchors", +"Link": "Link", +"Paste or type a link": "Paste or type a link", +"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?", +"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?", +"Link list": "Link list", +"Insert video": "Insert video", +"Insert\/edit video": "Insert\/edit video", +"Insert\/edit media": "Insert\/edit media", +"Alternative source": "Alternative source", +"Poster": "Poster", +"Paste your embed code below:": "Paste your embed code below:", +"Embed": "Embed", +"Media": "Media", +"Nonbreaking space": "Nonbreaking space", +"Page break": "Page break", +"Paste as text": "Paste as text", +"Preview": "Preview", +"Print": "Print", +"Save": "Save", +"Find": "Find", +"Replace with": "Replace with", +"Replace": "Replace", +"Replace all": "Replace all", +"Prev": "Prev", +"Next": "Next", +"Find and replace": "Find and replace", +"Could not find the specified string.": "Could not find the specified string.", +"Match case": "Match case", +"Whole words": "Whole words", +"Spellcheck": "Spellcheck", +"Ignore": "Ignore", +"Ignore all": "Ignore all", +"Finish": "Finish", +"Add to Dictionary": "Add to Dictionary", +"Insert table": "Insert table", +"Table properties": "Table properties", +"Delete table": "Delete table", +"Cell": "Cell", +"Row": "Row", +"Column": "Column", +"Cell properties": "Cell properties", +"Merge cells": "Merge cells", +"Split cell": "Split cell", +"Insert row before": "Insert row before", +"Insert row after": "Insert row after", +"Delete row": "Delete row", +"Row properties": "Row properties", +"Cut row": "Cut row", +"Copy row": "Copy row", +"Paste row before": "Paste row before", +"Paste row after": "Paste row after", +"Insert column before": "Insert column before", +"Insert column after": "Insert column after", +"Delete column": "Delete column", +"Cols": "Cols", +"Rows": "Rows", +"Width": "Width", +"Height": "Height", +"Cell spacing": "Cell spacing", +"Cell padding": "Cell padding", +"Caption": "Caption", +"Left": "Left", +"Center": "Center", +"Right": "Right", +"Cell type": "Cell type", +"Scope": "Scope", +"Alignment": "Alignment", +"H Align": "H Align", +"V Align": "V Align", +"Top": "Top", +"Middle": "Middle", +"Bottom": "Bottom", +"Header cell": "Header cell", +"Row group": "Row group", +"Column group": "Column group", +"Row type": "Row type", +"Header": "Header", +"Body": "Body", +"Footer": "Footer", +"Border color": "Border color", +"Insert template": "Insert template", +"Templates": "Templates", +"Template": "Template", +"Text color": "Text color", +"Background color": "Background color", +"Custom...": "Custom...", +"Custom color": "Custom color", +"No color": "No color", +"Table of Contents": "Table of Contents", +"Show blocks": "Show blocks", +"Show invisible characters": "Show invisible characters", +"Words: {0}": "Words: {0}", +"{0} words": "{0} words", +"File": "File", +"Edit": "Edit", +"Insert": "Insert", +"View": "View", +"Format": "Format", +"Table": "Table", +"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/package.json b/src/Umbraco.Web.UI.Client/package.json index 8ab5980107..c298f063a7 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -42,7 +42,7 @@ "npm": "6.13.6", "signalr": "2.4.0", "spectrum-colorpicker": "1.8.0", - "tinymce": "4.9.9", + "tinymce": "4.9.10", "typeahead.js": "0.11.1", "underscore": "1.9.1" }, 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 9117ab548c..cb5a9c712a 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 @@ -63,10 +63,14 @@ vm.labels = {}; localizationService.localizeMany([ vm.usernameIsEmail ? "general_email" : "general_username", - vm.usernameIsEmail ? "placeholders_email" : "placeholders_usernameHint"] + vm.usernameIsEmail ? "placeholders_email" : "placeholders_usernameHint", + vm.usernameIsEmail ? "placeholders_emptyEmail" : "placeholders_emptyUsername", + "placeholders_emptyPassword"] ).then(function (data) { vm.labels.usernameLabel = data[0]; vm.labels.usernamePlaceholder = data[1]; + vm.labels.usernameError = data[2]; + vm.labels.passwordError = data[3]; }); vm.twoFactor = {}; @@ -193,70 +197,70 @@ } function loginSubmit() { - - // make sure that we are returning to the login view. - vm.view = "login"; - - // TODO: Do validation properly like in the invite password update + + if (formHelper.submitForm({ scope: $scope })) { + //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) { + 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"; - //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) { - vm.loginForm.username.$setValidity('auth', true); - vm.loginForm.password.$setValidity('auth', true); - } + vm.loginStates.submitButton = "busy"; - if (vm.loginForm.$invalid) { - return; - } + userService.authenticate(vm.login, vm.password) + .then(function(data) { + vm.loginStates.submitButton = "success"; + userService._retryRequestQueue(true); + if (vm.onLogin) { + vm.onLogin(); + } + }, + function(reason) { - vm.loginStates.submitButton = "busy"; + //is Two Factor required? + if (reason.status === 402) { + vm.errorMsg = "Additional authentication required"; + show2FALoginDialog(reason.data.twoFactorView); + } else { + vm.loginStates.submitButton = "error"; + vm.errorMsg = reason.errorMsg; - userService.authenticate(vm.login, vm.password) - .then(function (data) { - vm.loginStates.submitButton = "success"; - userService._retryRequestQueue(true); - if(vm.onLogin) { - vm.onLogin(); + //set the form inputs to invalid + vm.loginForm.username.$setValidity("auth", false); + vm.loginForm.password.$setValidity("auth", false); + } + + userService._retryRequestQueue(); + + }); + + //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() { + if (vm.loginForm.$invalid) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); } - }, - function (reason) { - - //is Two Factor required? - if (reason.status === 402) { - vm.errorMsg = "Additional authentication required"; - show2FALoginDialog(reason.data.twoFactorView); - } - else { - vm.loginStates.submitButton = "error"; - vm.errorMsg = reason.errorMsg; - - //set the form inputs to invalid - vm.loginForm.username.$setValidity("auth", false); - vm.loginForm.password.$setValidity("auth", false); - } - - userService._retryRequestQueue(); - }); - - //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 () { - if (vm.loginForm.$invalid) { - vm.loginForm.username.$setValidity('auth', true); - vm.loginForm.password.$setValidity('auth', true); - } - }); - vm.loginForm.password.$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() { + if (vm.loginForm.$invalid) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + }); + } } function requestPasswordResetSubmit(email) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js index 87053c083c..58f799e5af 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js @@ -206,7 +206,7 @@ Use this directive to construct a header inside the main editor window. (function () { 'use strict'; - function EditorHeaderDirective(editorService, localizationService, editorState) { + function EditorHeaderDirective(editorService, localizationService, editorState, $rootScope) { function link(scope, $injector) { @@ -224,27 +224,9 @@ Use this directive to construct a header inside the main editor window. if (editorState.current) { //to do make work for user create/edit // to do make it work for user group create/ edit - // to do make it work for language edit/create - // to do make it work for log viewer - scope.isNew = editorState.current.id === 0 || - editorState.current.id === "0" || - editorState.current.id === -1 || - editorState.current.id === 0 || - editorState.current.id === "-1"; - - var localizeVars = [ - scope.isNew ? "visuallyHiddenTexts_createItem" : "visuallyHiddenTexts_edit", - "visuallyHiddenTexts_name", - scope.isNew ? "general_new" : "general_edit" - ]; - - if (scope.editorfor) { - localizeVars.push(scope.editorfor); - } - localizationService.localizeMany(localizeVars).then(function(data) { - setAccessibilityForEditor(data); - scope.loading = false; - }); + // to make it work for language edit/create + setAccessibilityForEditorState(); + scope.loading = false; } else { scope.loading = false; } @@ -283,59 +265,91 @@ Use this directive to construct a header inside the main editor window. editorService.iconPicker(iconPicker); }; - function setAccessibilityForEditor(data) { - - if (editorState.current) { - if (scope.nameLocked) { - scope.accessibility.a11yName = scope.name; - SetPageTitle(scope.name); - } else { - - scope.accessibility.a11yMessage = data[0]; - scope.accessibility.a11yName = data[1]; - var title = data[2] + ":"; - if (!scope.isNew) { - scope.accessibility.a11yMessage += " " + scope.name; - title += " " + scope.name; - } else { - var name = ""; - if (editorState.current.contentTypeName) { - name = editorState.current.contentTypeName; - } else if (scope.editorfor) { - name = data[3]; - } - if (name !== "") { - scope.accessibility.a11yMessage += " " + name; - scope.accessibility.a11yName = name + " " + scope.accessibility.a11yName; - title += " " + name; - } - } - if (title !== data[2] + ":") { - SetPageTitle(title); - } - - } - scope.accessibility.a11yMessageVisible = !isEmptyOrSpaces(scope.accessibility.a11yMessage); - scope.accessibility.a11yNameVisible = !isEmptyOrSpaces(scope.accessibility.a11yName); + function setAccessibilityForEditorState() { + var isNew = editorState.current.id === 0 || + editorState.current.id === "0" || + editorState.current.id === -1 || + editorState.current.id === 0 || + editorState.current.id === "-1"; + + var contentTypeName = ""; + if (editorState.current.contentTypeName) { + contentTypeName = editorState.current.contentTypeName; } - + + var setTitle = false; + if (scope.setpagetitle !== undefined) { + setTitle = scope.setpagetitle; + } + setAccessibilityHeaderDirective(isNew, scope.editorfor, scope.nameLocked, scope.name, contentTypeName, setTitle); } + function setAccessibilityHeaderDirective(isNew, editorFor, nameLocked, entityName, contentTypeName, setTitle) { + + var localizeVars = [ + isNew ? "visuallyHiddenTexts_createItem" : "visuallyHiddenTexts_edit", + "visuallyHiddenTexts_name", + isNew ? "general_new" : "general_edit" + ]; + + if (editorFor) { + localizeVars.push(editorFor); + } + localizationService.localizeMany(localizeVars).then(function(data) { + if (nameLocked) { + scope.accessibility.a11yName = entityName; + if (setTitle) { + SetPageTitle(entityName); + } + } else { + + scope.accessibility.a11yMessage = data[0]; + scope.accessibility.a11yName = data[1]; + var title = data[2] + ":"; + if (!isNew) { + scope.accessibility.a11yMessage += " " + entityName; + title += " " + entityName; + } else { + var name = ""; + if (contentTypeName) { + name = editorState.current.contentTypeName; + } else if (editorFor) { + name = data[3]; + } + if (name !== "") { + scope.accessibility.a11yMessage += " " + name; + scope.accessibility.a11yName = name + " " + scope.accessibility.a11yName; + title += " " + name; + } + } + if (setTitle && title !== data[2] + ":") { + SetPageTitle(title); + } + + } + scope.accessibility.a11yMessageVisible = !isEmptyOrSpaces(scope.accessibility.a11yMessage); + scope.accessibility.a11yNameVisible = !isEmptyOrSpaces(scope.accessibility.a11yName); + + }); + } + + + function isEmptyOrSpaces(str) { return str === null || str===undefined || str.trim ===''; } function SetPageTitle(title) { - var setTitle = false; - if (scope.setpagetitle !== undefined) { - setTitle = scope.setpagetitle; - } - if (setTitle) { scope.$emit("$changeTitle", title); - } } + + $rootScope.$on('$setAccessibleHeader', function (event, isNew, editorFor, nameLocked, name, contentTypeName, setTitle) { + setAccessibilityHeaderDirective(isNew, editorFor, nameLocked, name, contentTypeName, setTitle); + }); } + + var directive = { transclude: true, restrict: 'E', diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js index 9a9d6d4a76..389aec2044 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js @@ -32,6 +32,7 @@ @param {boolean} required Set the checkbox to be required. @param {callback} onChange Callback when the value of the checkbox change by interaction. @param {string} cssClass Set a css class modifier +@param {boolean} disableDirtyCheck Disable checking if the model is dirty **/ @@ -84,7 +85,8 @@ required: "<", onChange: "&?", cssClass: "@?", - iconClass: "@?" + iconClass: "@?", + disableDirtyCheck: "=?" } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index 9c33b35e82..ad62bcd3db 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -16,10 +16,15 @@ angular.module("umbraco.directives") replace: true, templateUrl: 'views/components/property/umb-property.html', link: function (scope) { - userService.getCurrentUser().then(function (u) { - var isAdmin = u.userGroups.indexOf('admin') !== -1; - scope.propertyAlias = (Umbraco.Sys.ServerVariables.isDebuggingEnabled === true || isAdmin) ? scope.property.alias : null; - }); + + scope.controlLabelTitle = null; + if(Umbraco.Sys.ServerVariables.isDebuggingEnabled) { + userService.getCurrentUser().then(function (u) { + if(u.allowedSections.indexOf("settings") !== -1 ? true : false) { + scope.controlLabelTitle = scope.property.alias; + } + }); + } }, //Define a controller for this directive to expose APIs to other directives controller: function ($scope) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js index 9f1f7a0d2e..fd52c4d7ea 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js @@ -102,10 +102,15 @@ if (!scope.editLabelKey) { scope.editLabelKey = "general_edit"; } - userService.getCurrentUser().then(function (u) { - var isAdmin = u.userGroups.indexOf('admin') !== -1; - scope.alias = (Umbraco.Sys.ServerVariables.isDebuggingEnabled === true || isAdmin) ? scope.alias : null; - }); + + scope.nodeNameTitle = null; + if(Umbraco.Sys.ServerVariables.isDebuggingEnabled) { + userService.getCurrentUser().then(function (u) { + if (u.allowedSections.indexOf("settings") !== -1 ? true : false) { + scope.nodeNameTitle = scope.alias; + } + }); + } } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js index 96a072330b..653b4f427c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js @@ -26,6 +26,7 @@ fileManager.setFiles({ propertyAlias: vm.propertyAlias, culture: vm.culture, + segment: vm.segment, files: [] }); //clear the current files @@ -92,6 +93,11 @@ vm.culture = null; } + //normalize segment to null if it's not there + if (!vm.segment) { + vm.segment = null; + } + // TODO: need to figure out what we can do for things like Nested Content var existingClientFiles = checkPendingClientFiles(); @@ -134,11 +140,16 @@ vm.culture = null; } + //normalize segment to null if it's not there + if (!vm.segment) { + vm.segment = null; + } + //check the file manager to see if there's already local files pending for this editor var existingClientFiles = _.map( _.filter(fileManager.getFiles(), function (f) { - return f.alias === vm.propertyAlias && f.culture === vm.culture; + return f.alias === vm.propertyAlias && f.culture === vm.culture && f.segment === vm.segment; }), function (f) { return f.file; @@ -264,7 +275,8 @@ fileManager.setFiles({ propertyAlias: vm.propertyAlias, files: args.files, - culture: vm.culture + culture: vm.culture, + segment: vm.segment }); updateModelFromSelectedFiles(args.files).then(function(newVal) { @@ -287,6 +299,7 @@ templateUrl: 'views/components/upload/umb-property-file-upload.html', bindings: { culture: "@?", + segment: "@?", propertyAlias: "@", value: "<", hideSelection: "<", diff --git a/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js b/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js index 9e0285d58d..38aee3fc4a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js @@ -39,18 +39,22 @@ function fileManager($rootScope) { args.culture = null; } + if (!args.segment) { + args.segment = null; + } + var metaData = []; if (Utilities.isArray(args.metaData)) { metaData = args.metaData; } - //this will clear the files for the current property/culture and then add the new ones for the current property + //this will clear the files for the current property/culture/segment and then add the new ones for the current property fileCollection = _.reject(fileCollection, function (item) { - return item.alias === args.propertyAlias && (!args.culture || args.culture === item.culture); + return item.alias === args.propertyAlias && (!args.culture || args.culture === item.culture) && (!args.segment || args.segment === item.segment); }); for (var i = 0; i < args.files.length; i++) { //save the file object to the files collection - fileCollection.push({ alias: args.propertyAlias, file: args.files[i], culture: args.culture, metaData: metaData }); + fileCollection.push({ alias: args.propertyAlias, file: args.files[i], culture: args.culture, segment: args.segment, metaData: metaData }); } }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index edf698c8a7..4cbc5e567a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -252,12 +252,13 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe for (var f in args.files) { //each item has a property alias and the file object, we'll ensure that the alias is suffixed to the key // so we know which property it belongs to on the server side - var fileKey = "file_" + args.files[f].alias + "_" + (args.files[f].culture ? args.files[f].culture : ""); + var file = args.files[f]; + var fileKey = "file_" + file.alias + "_" + (file.culture ? file.culture : "") + "_" + (file.segment ? file.segment : ""); - if (Utilities.isArray(args.files[f].metaData) && args.files[f].metaData.length > 0) { - fileKey += ("_" + args.files[f].metaData.join("_")); + if (Utilities.isArray(file.metaData) && file.metaData.length > 0) { + fileKey += ("_" + file.metaData.join("_")); } - formData.append(fileKey, args.files[f].file); + formData.append(fileKey, file.file); } }).then(function (response) { //success callback diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 174f9f41d7..f0d7c6f1e1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -227,6 +227,7 @@ @import "dashboards/umbraco-forms.less"; @import "dashboards/examine-management.less"; @import "dashboards/healthcheck.less"; +@import "dashboards/content-templates.less"; @import "dashboards/nucache.less"; @import "typeahead.less"; 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 a52f81b92a..9a3760444d 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 @@ -18,6 +18,8 @@ label.umb-form-check--checkbox{ } .umb-form-check__info { margin-left:20px; + position: relative; + top: 3px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards/content-templates.less b/src/Umbraco.Web.UI.Client/src/less/dashboards/content-templates.less new file mode 100644 index 0000000000..9966fc97e1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/dashboards/content-templates.less @@ -0,0 +1,22 @@ +.content-templates-dashboard{ + p{ + line-height: 1.6em; + margin-bottom: 30px; + + &:last-child{ + margin-bottom: 0; + } + } + + ul{ + margin-bottom: 15px; + } + + li{ + margin-bottom: 5px; + + &:last-child{ + margin-bottom: 0; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/login.less b/src/Umbraco.Web.UI.Client/src/less/pages/login.less index e36acdc273..818b1d84d1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -123,6 +123,7 @@ position: relative; text-align: right; user-select: none; + margin-left: auto; a { opacity: .5; @@ -134,8 +135,8 @@ .password-text { background-repeat: no-repeat; background-size: 18px; - background-position: left center; - padding-left: 26px; + background-position: 0px 1px; + padding-left: 24px; &.show { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath fill='%23444' d='M16 6C9 6 3 10 0 16c3 6 9 10 16 10s13-4 16-10c-3-6-9-10-16-10zm8 5.3c1.8 1.2 3.4 2.8 4.6 4.7-1.2 2-2.8 3.5-4.7 4.7-3 1.5-6 2.3-8 2.3s-6-.8-8-2.3C6 19.5 4 18 3 16c1.5-2 3-3.5 5-4.7l.6-.2C8 12 8 13 8 14c0 4.5 3.5 8 8 8s8-3.5 8-8c0-1-.3-2-.6-2.6l.4.3zM16 13c0 1.7-1.3 3-3 3s-3-1.3-3-3 1.3-3 3-3 3 1.3 3 3z'/%3E%3C/svg%3E"); diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 764b73c593..b5870b8dce 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -863,11 +863,11 @@ .bootstrap-datetimepicker-widget .picker-switch .btn{ background: none; border: none;} .umb-datepicker .input-append .add-on{cursor: pointer;} .umb-datepicker .input-append .on-top { + border: 0 none; position: absolute; margin-left: -31px; margin-top: 1px; display: inline-block; - height: 22px; padding: 5px 6px 3px 6px; font-size: @baseFontSize; font-weight: normal; diff --git a/src/Umbraco.Web.UI.Client/src/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js index 81eadf150f..297d93f4bc 100644 --- a/src/Umbraco.Web.UI.Client/src/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/main.controller.js @@ -56,12 +56,14 @@ function MainController($scope, $location, appState, treeService, notificationsS appState.setSearchState("show", false); }; - $scope.showLoginScreen = function(isTimedOut) { + $scope.showLoginScreen = function (isTimedOut) { + $scope.login.pageTitle = $scope.$root.locationTitle; $scope.login.isTimedOut = isTimedOut; $scope.login.show = true; }; - $scope.hideLoginScreen = function() { + $scope.hideLoginScreen = function () { + $scope.$root.locationTitle = $scope.login.pageTitle; $scope.login.show = false; }; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html index aa99248dfc..06162d2961 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html @@ -18,15 +18,14 @@
- +