diff --git a/src/Umbraco.Core/Composing/CompositionRoots/CoreMappingProfilesCompositionRoot.cs b/src/Umbraco.Core/Composing/CompositionRoots/CoreMappingProfilesCompositionRoot.cs index b68aef3c3d..6b55a4af7e 100644 --- a/src/Umbraco.Core/Composing/CompositionRoots/CoreMappingProfilesCompositionRoot.cs +++ b/src/Umbraco.Core/Composing/CompositionRoots/CoreMappingProfilesCompositionRoot.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Composing.CompositionRoots { public void Compose(IServiceRegistry container) { - container.Register(); + container.Register(); } } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index 2dcbe06458..332de45734 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { internal class ContentElement : UmbracoConfigurationElement, IContentSection { - private const string DefaultPreviewBadge = @"In Preview Mode - click to end"; + private const string DefaultPreviewBadge = @"In Preview Mode - click to end"; [ConfigurationProperty("imaging")] internal ContentImagingElement Imaging => (ContentImagingElement) this["imaging"]; diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 75ed8238d0..302ccbcc54 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -33,6 +33,8 @@ namespace Umbraco.Core.Models.Identity private string[] _allowedSections; private int[] _startMediaIds; private int[] _startContentIds; + private DateTime? _lastPasswordChangeDateUtc; + /// /// Used to construct a new instance without an identity @@ -136,6 +138,15 @@ namespace Umbraco.Core.Models.Identity set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _userName, Ps.Value.UserNameSelector); } + /// + /// LastPasswordChangeDateUtc so we can track changes to it + /// + public override DateTime? LastPasswordChangeDateUtc + { + get { return _lastPasswordChangeDateUtc; } + set { _beingDirty.SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangeDateUtc, Ps.Value.LastPasswordChangeDateUtcSelector); } + } + /// /// Override LastLoginDateUtc so we can track changes to it /// @@ -419,6 +430,7 @@ namespace Umbraco.Core.Models.Identity public readonly PropertyInfo EmailSelector = ExpressionHelper.GetPropertyInfo(x => x.Email); public readonly PropertyInfo UserNameSelector = ExpressionHelper.GetPropertyInfo(x => x.UserName); public readonly PropertyInfo LastLoginDateUtcSelector = ExpressionHelper.GetPropertyInfo(x => x.LastLoginDateUtc); + public readonly PropertyInfo LastPasswordChangeDateUtcSelector = ExpressionHelper.GetPropertyInfo(x => x.LastPasswordChangeDateUtc); public readonly PropertyInfo EmailConfirmedSelector = ExpressionHelper.GetPropertyInfo(x => x.EmailConfirmed); public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); public readonly PropertyInfo AccessFailedCountSelector = ExpressionHelper.GetPropertyInfo(x => x.AccessFailedCount); @@ -439,5 +451,6 @@ namespace Umbraco.Core.Models.Identity groups => groups.GetHashCode()); } + } } diff --git a/src/Umbraco.Core/Models/Identity/IdentityProfile.cs b/src/Umbraco.Core/Models/Identity/IdentityMapperProfile.cs similarity index 90% rename from src/Umbraco.Core/Models/Identity/IdentityProfile.cs rename to src/Umbraco.Core/Models/Identity/IdentityMapperProfile.cs index f44003b62a..81069bd74c 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityProfile.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityMapperProfile.cs @@ -8,9 +8,9 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Models.Identity { - public class IdentityProfile : Profile + public class IdentityMapperProfile : Profile { - public IdentityProfile(ILocalizedTextService textService, IEntityService entityService, IGlobalSettings globalSettings) + public IdentityMapperProfile(ILocalizedTextService textService, IEntityService entityService, IGlobalSettings globalSettings) { CreateMap() .BeforeMap((src, dest) => @@ -19,6 +19,7 @@ namespace Umbraco.Core.Models.Identity }) .ConstructUsing(src => new BackOfficeIdentityUser(src.Id, src.Groups)) .ForMember(dest => dest.LastLoginDateUtc, opt => opt.MapFrom(src => src.LastLoginDate.ToUniversalTime())) + .ForMember(user => user.LastPasswordChangeDateUtc, expression => expression.MapFrom(user => user.LastPasswordChangeDate.ToUniversalTime())) .ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email)) .ForMember(dest => dest.EmailConfirmed, opt => opt.MapFrom(src => src.EmailConfirmedDate.HasValue)) .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs index 69b9e26385..b8ed355c55 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUser.cs @@ -75,6 +75,12 @@ namespace Umbraco.Core.Models.Identity /// public virtual DateTime? LockoutEndDateUtc { get; set; } + /// + /// DateTime in UTC when the password was last changed. + /// + /// + public virtual DateTime? LastPasswordChangeDateUtc { get; set; } + /// /// Is lockout enabled for this user /// diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 8324ec11a0..c4406054e6 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -434,6 +434,7 @@ namespace Umbraco.Core.Security /// protected override async Task UpdatePassword(IUserPasswordStore passwordStore, T user, string newPassword) { + user.LastPasswordChangeDateUtc = DateTime.UtcNow; var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher; if (userAwarePasswordHasher == null) return await base.UpdatePassword(passwordStore, user, newPassword); @@ -484,15 +485,22 @@ namespace Umbraco.Core.Security #endregion - public override Task SetLockoutEndDateAsync(int userId, DateTimeOffset lockoutEnd) + public override async Task SetLockoutEndDateAsync(int userId, DateTimeOffset lockoutEnd) { - var result = base.SetLockoutEndDateAsync(userId, lockoutEnd); + var result = await base.SetLockoutEndDateAsync(userId, lockoutEnd); // The way we unlock is by setting the lockoutEnd date to the current datetime - if (result.Result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow) + if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow) + { RaiseAccountLockedEvent(userId); + } else + { RaiseAccountUnlockedEvent(userId); + //Resets the login attempt fails back to 0 when unlock is clicked + await ResetAccessFailedCountAsync(userId); + + } return result; } @@ -517,13 +525,36 @@ namespace Umbraco.Core.Security - - public override Task AccessFailedAsync(int userId) + /// + /// Overides the microsoft ASP.NET user managment method + /// + /// + /// + /// returns a Async Task + /// + /// + /// Doesnt set fail attempts back to 0 + /// + public override async Task AccessFailedAsync(int userId) { - var result = base.AccessFailedAsync(userId); + var lockoutStore = (IUserLockoutStore)Store; + var user = await FindByIdAsync(userId); + if (user == null) + throw new InvalidOperationException("No user found by user id " + userId); + + var count = await lockoutStore.IncrementAccessFailedCountAsync(user); + + if (count >= MaxFailedAccessAttemptsBeforeLockout) + { + await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(DefaultAccountLockoutTimeSpan)); + //NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0 + //here we are persisting the value for the back office + } + + var result = await UpdateAsync(user); //Slightly confusing: this will return a Success if we successfully update the AccessFailed count - if (result.Result.Succeeded) + if (result.Succeeded) RaiseLoginFailedEvent(userId); return result; diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 2f70f32d89..63970aca8f 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -637,6 +637,13 @@ namespace Umbraco.Core.Security anythingChanged = true; user.LastLoginDate = identityUser.LastLoginDateUtc.Value.ToLocalTime(); } + if (identityUser.IsPropertyDirty("LastPasswordChangeDateUtc") + || (user.LastPasswordChangeDate != default(DateTime) && identityUser.LastPasswordChangeDateUtc.HasValue == false) + || identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value) + { + anythingChanged = true; + user.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime(); + } if (identityUser.IsPropertyDirty("EmailConfirmed") || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default(DateTime) && identityUser.EmailConfirmed == false) || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default(DateTime)) && identityUser.EmailConfirmed)) diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs index a61faa10c7..d77b2080ee 100644 --- a/src/Umbraco.Core/Services/IFileService.cs +++ b/src/Umbraco.Core/Services/IFileService.cs @@ -358,5 +358,6 @@ namespace Umbraco.Core.Services /// The name of the snippet /// The content of the partial view. string GetPartialViewSnippetContent(string snippetName); + } } diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index e953a6073e..30e76468a6 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1841,7 +1841,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(TreeChanged, this, new TreeChange(copy, TreeChangeTypes.RefreshBranch).ToEventArgs()); foreach (var x in copies) scope.Events.Dispatch(Copied, this, new CopyEventArgs(x.Item1, x.Item2, false, x.Item2.ParentId, relateToOriginal)); - Audit(AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); + Audit(AuditType.Copy, "Copy Content performed by user", userId, content.Id); scope.Complete(); } diff --git a/src/Umbraco.Core/Services/Implement/FileService.cs b/src/Umbraco.Core/Services/Implement/FileService.cs index 09bd096f8e..4b95a0c1c3 100644 --- a/src/Umbraco.Core/Services/Implement/FileService.cs +++ b/src/Umbraco.Core/Services/Implement/FileService.cs @@ -332,16 +332,23 @@ namespace Umbraco.Core.Services.Implement var evtMsgs = EventMessagesFactory.Get(); - //NOTE: This isn't pretty but we need to maintain backwards compatibility so we cannot change + //fixme: This isn't pretty because we we're required to maintain backwards compatibility so we could not change // the event args here. The other option is to create a different event with different event - // args specifically for this method... which also isn't pretty. So for now, we'll use this - // dictionary approach to store 'additional data' in. + // args specifically for this method... which also isn't pretty. So fix this in v8! var additionalData = new Dictionary { { "CreateTemplateForContentType", true }, { "ContentTypeAlias", contentTypeAlias }, }; + // check that the template hasn't been created on disk before creating the content type + // if it exists, set the new template content to the existing file content + string content = GetViewContent(contentTypeAlias); + if (content.IsNullOrWhiteSpace() == false) + { + template.Content = content; + } + using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(template, true, evtMsgs, additionalData); @@ -368,6 +375,15 @@ namespace Umbraco.Core.Services.Implement { Content = content }; + + // check that the template hasn't been created on disk before creating the content type + // if it exists, set the new template content to the existing file content + string existingContent = GetViewContent(template.Alias); + if (existingContent.IsNullOrWhiteSpace() == false) + { + template.Content = content; + } + if (masterTemplate != null) { template.SetMasterTemplate(masterTemplate); @@ -659,10 +675,26 @@ namespace Umbraco.Core.Services.Implement return _templateRepository.GetFileSize(filepath); } } + + private string GetViewContent(string fileName) + { + if (fileName.IsNullOrWhiteSpace()) + throw new ArgumentNullException(nameof(fileName)); - #endregion + if (!fileName.EndsWith(".cshtml")) + fileName = string.Concat(fileName, ".cshtml"); - #region Partial Views + var fs = _templateRepository.GetFileContentStream(fileName); + if (fs == null) return string.Empty; + using (var view = new StreamReader(fs)) + { + return view.ReadToEnd().Trim(); + } + } + +#endregion + +#region Partial Views public IEnumerable GetPartialViewSnippetNames(params string[] filterNames) { diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 4c2dd2df3f..1689be9b85 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -355,6 +355,7 @@ + @@ -663,7 +664,6 @@ - diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs index fe32a43cc9..f1ac463305 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs @@ -16,28 +16,28 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings [Test] public void EmailAddress() { - Assert.IsTrue(SettingsSection.Content.NotificationEmailAddress == "robot@umbraco.dk"); + Assert.AreEqual(SettingsSection.Content.NotificationEmailAddress, "robot@umbraco.dk"); } [Test] public virtual void DisableHtmlEmail() { - Assert.IsTrue(SettingsSection.Content.DisableHtmlEmail == true); + Assert.IsTrue(SettingsSection.Content.DisableHtmlEmail); } [Test] public virtual void Can_Set_Multiple() { - Assert.IsTrue(SettingsSection.Content.Error404Collection.Count() == 3); - Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(0).Culture == "default"); - Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(0).ContentId == 1047); + Assert.AreEqual(SettingsSection.Content.Error404Collection.Count(), 3); + Assert.AreEqual(SettingsSection.Content.Error404Collection.ElementAt(0).Culture, "default"); + Assert.AreEqual(SettingsSection.Content.Error404Collection.ElementAt(0).ContentId, 1047); Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(0).HasContentId); Assert.IsFalse(SettingsSection.Content.Error404Collection.ElementAt(0).HasContentKey); - Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(1).Culture == "en-US"); - Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(1).ContentXPath == "$site/error [@name = 'error']"); + Assert.AreEqual(SettingsSection.Content.Error404Collection.ElementAt(1).Culture, "en-US"); + Assert.AreEqual(SettingsSection.Content.Error404Collection.ElementAt(1).ContentXPath, "$site/error [@name = 'error']"); Assert.IsFalse(SettingsSection.Content.Error404Collection.ElementAt(1).HasContentId); Assert.IsFalse(SettingsSection.Content.Error404Collection.ElementAt(1).HasContentKey); - Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(2).Culture == "en-UK"); - Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(2).ContentKey == new Guid("8560867F-B88F-4C74-A9A4-679D8E5B3BFC")); + Assert.AreEqual(SettingsSection.Content.Error404Collection.ElementAt(2).Culture, "en-UK"); + Assert.AreEqual(SettingsSection.Content.Error404Collection.ElementAt(2).ContentKey, new Guid("8560867F-B88F-4C74-A9A4-679D8E5B3BFC")); Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(2).HasContentKey); Assert.IsFalse(SettingsSection.Content.Error404Collection.ElementAt(2).HasContentId); } @@ -45,7 +45,7 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings [Test] public void ScriptFolderPath() { - Assert.IsTrue(SettingsSection.Content.ScriptFolderPath == "/scripts"); + Assert.AreEqual(SettingsSection.Content.ScriptFolderPath, "/scripts"); } [Test] public void ScriptFileTypes() @@ -55,7 +55,7 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings [Test] public void DisableScriptEditor() { - Assert.IsTrue(SettingsSection.Content.ScriptEditorDisable == false); + Assert.AreEqual(SettingsSection.Content.ScriptEditorDisable, false); } [Test] @@ -71,80 +71,79 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings [Test] public virtual void ImageAutoFillProperties() { - Assert.IsTrue(SettingsSection.Content.ImageAutoFillProperties.Count() == 2); - Assert.IsTrue(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).Alias == "umbracoFile"); - Assert.IsTrue(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).WidthFieldAlias == "umbracoWidth"); - Assert.IsTrue(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).HeightFieldAlias == "umbracoHeight"); - Assert.IsTrue(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).LengthFieldAlias == "umbracoBytes"); - Assert.IsTrue(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).ExtensionFieldAlias == "umbracoExtension"); - Assert.IsTrue(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).Alias == "umbracoFile2"); - Assert.IsTrue(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).WidthFieldAlias == "umbracoWidth2"); - Assert.IsTrue(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).HeightFieldAlias == "umbracoHeight2"); - Assert.IsTrue(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).LengthFieldAlias == "umbracoBytes2"); - Assert.IsTrue(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).ExtensionFieldAlias == "umbracoExtension2"); + Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.Count(), 2); + Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).Alias, "umbracoFile"); + Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).WidthFieldAlias, "umbracoWidth"); + Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).HeightFieldAlias, "umbracoHeight"); + Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).LengthFieldAlias, "umbracoBytes"); + Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).ExtensionFieldAlias, "umbracoExtension"); + Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).Alias, "umbracoFile2"); + Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).WidthFieldAlias, "umbracoWidth2"); + Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).HeightFieldAlias, "umbracoHeight2"); + Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).LengthFieldAlias, "umbracoBytes2"); + Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).ExtensionFieldAlias, "umbracoExtension2"); } [Test] public void UploadAllowDirectories() { - Assert.IsTrue(SettingsSection.Content.UploadAllowDirectories == true); + Assert.IsTrue(SettingsSection.Content.UploadAllowDirectories); } [Test] public void DefaultDocumentTypeProperty() { - Assert.IsTrue(SettingsSection.Content.DefaultDocumentTypeProperty == "Textstring"); + Assert.AreEqual(SettingsSection.Content.DefaultDocumentTypeProperty, "Textstring"); } [Test] public void GlobalPreviewStorageEnabled() { - Assert.IsTrue(SettingsSection.Content.GlobalPreviewStorageEnabled == false); + Assert.IsFalse(SettingsSection.Content.GlobalPreviewStorageEnabled); } [Test] public void CloneXmlContent() { - Assert.IsTrue(SettingsSection.Content.CloneXmlContent == true); + Assert.IsTrue(SettingsSection.Content.CloneXmlContent); } [Test] public void EnsureUniqueNaming() { - Assert.IsTrue(SettingsSection.Content.EnsureUniqueNaming == true); + Assert.IsTrue(SettingsSection.Content.EnsureUniqueNaming); } - [Test] public void ForceSafeAliases() { - Assert.IsTrue(SettingsSection.Content.ForceSafeAliases == true); + Assert.IsTrue(SettingsSection.Content.ForceSafeAliases); } [Test] public void XmlCacheEnabled() { - Assert.IsTrue(SettingsSection.Content.XmlCacheEnabled == true); + Assert.IsTrue(SettingsSection.Content.XmlCacheEnabled); } [Test] public void ContinouslyUpdateXmlDiskCache() { - Assert.IsTrue(SettingsSection.Content.ContinouslyUpdateXmlDiskCache == true); + Assert.IsTrue(SettingsSection.Content.ContinouslyUpdateXmlDiskCache); } [Test] public virtual void XmlContentCheckForDiskChanges() { - Assert.IsTrue(SettingsSection.Content.XmlContentCheckForDiskChanges == true); + Assert.IsTrue(SettingsSection.Content.XmlContentCheckForDiskChanges); } [Test] public void EnableSplashWhileLoading() { - Assert.IsTrue(SettingsSection.Content.EnableSplashWhileLoading == false); + Assert.IsFalse(SettingsSection.Content.EnableSplashWhileLoading); } [Test] public void PropertyContextHelpOption() { - Assert.IsTrue(SettingsSection.Content.PropertyContextHelpOption == "text"); + Assert.AreEqual(SettingsSection.Content.PropertyContextHelpOption, "text"); } [Test] public void PreviewBadge() { - Assert.IsTrue(SettingsSection.Content.PreviewBadge == @"In Preview Mode - click to end"); + Assert.AreEqual(SettingsSection.Content.PreviewBadge, @"In Preview Mode - click to end"); } [Test] public void ResolveUrlsFromTextString() @@ -154,7 +153,7 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings [Test] public void MacroErrors() { - Assert.IsTrue(SettingsSection.Content.MacroErrorBehaviour == MacroErrorBehaviour.Inline); + Assert.AreEqual(SettingsSection.Content.MacroErrorBehaviour, MacroErrorBehaviour.Inline); } [Test] diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config index 5373afd1b5..a436dad9f5 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config @@ -75,7 +75,10 @@ true - In Preview Mode - click to end]]> + + In Preview Mode - click to end + ]]> "; - //create an id class for this element so we can re-select it after inserting - var uniqueId = "umb-macro-" + editor.dom.uniqueId(); - var macroDiv = editor.dom.create('div', - { - 'class': 'umb-macro-holder ' + macroObject.macroAlias + ' mceNonEditable ' + uniqueId - }, - macroSyntaxComment + 'Macro alias: ' + macroObject.macroAlias + ''); - - editor.selection.setNode(macroDiv); - - var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId)); - - //async load the macro content - this.loadMacroContent($macroDiv, macroObject, $scope); - - }, - - /** loads in the macro content async from the server */ - loadMacroContent: function($macroDiv, macroData, $scope) { - - //if we don't have the macroData, then we'll need to parse it from the macro div - if (!macroData) { - var contents = $macroDiv.contents(); - var comment = _.find(contents, function (item) { - return item.nodeType === 8; - }); - if (!comment) { - throw "Cannot parse the current macro, the syntax in the editor is invalid"; - } - var syntax = comment.textContent.trim(); - var parsed = macroService.parseMacroSyntax(syntax); - macroData = parsed; - } - - var $ins = $macroDiv.find("ins"); - - //show the throbber - $macroDiv.addClass("loading"); - - var contentId = $routeParams.id; - - //need to wrap in safe apply since this might be occuring outside of angular - angularHelper.safeApply($scope, function() { - macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary) - .then(function (htmlResult) { - - $macroDiv.removeClass("loading"); - htmlResult = htmlResult.trim(); - if (htmlResult !== "") { - $ins.html(htmlResult); - } - }); - }); - - }, - - createLinkPicker: function(editor, $scope, onClick) { - - function createLinkList(callback) { - return function() { - var linkList = editor.settings.link_list; - - if (typeof(linkList) === "string") { - tinymce.util.XHR.send({ - url: linkList, - success: function(text) { - callback(tinymce.util.JSON.parse(text)); - } - }); - } else { - callback(linkList); - } - }; - } - - function showDialog(linkList) { - var data = {}, selection = editor.selection, dom = editor.dom, selectedElm, anchorElm, initialText; - var win, linkListCtrl, relListCtrl, targetListCtrl; - - function linkListChangeHandler(e) { - var textCtrl = win.find('#text'); - - if (!textCtrl.value() || (e.lastControl && textCtrl.value() === e.lastControl.text())) { - textCtrl.value(e.control.text()); - } - - win.find('#href').value(e.control.value()); - } - - function buildLinkList() { - var linkListItems = [{ - text: 'None', - value: '' +function tinyMceService($log, imageHelper, $http, $timeout, macroResource, macroService, $routeParams, umbRequestHelper, angularHelper, userService) { + return { + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#configuration + * @methodOf umbraco.services.tinyMceService + * + * @description + * Returns a collection of plugins available to the tinyMCE editor + * + */ + configuration: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "rteApiBaseUrl", + "GetConfiguration"), { + cache: true + }), + 'Failed to retrieve tinymce configuration'); + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#defaultPrevalues + * @methodOf umbraco.services.tinyMceService + * + * @description + * Returns a default configration to fallback on in case none is provided + * + */ + defaultPrevalues: function () { + var cfg = {}; + cfg.toolbar = ["code", "bold", "italic", "styleselect", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "outdent", "indent", "link", "image", "umbmediapicker", "umbembeddialog", "umbmacro"]; + cfg.stylesheets = []; + cfg.dimensions = { + height: 500 + }; + cfg.maxImageSize = 500; + return cfg; + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createInsertEmbeddedMedia + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the umbrco insert embedded media tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + * @param {Object} $scope the current controller scope + */ + createInsertEmbeddedMedia: function (editor, scope, callback) { + editor.addButton('umbembeddialog', { + icon: 'custom icon-tv', + tooltip: 'Embed', + onclick: function () { + if (callback) { + callback(); + } + } + }); + }, + + insertEmbeddedMediaInEditor: function (editor, preview) { + editor.insertContent(preview); + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createMediaPicker + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the umbrco insert media tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + * @param {Object} $scope the current controller scope + */ + createMediaPicker: function (editor, scope, callback) { + editor.addButton('umbmediapicker', { + icon: 'custom icon-picture', + tooltip: 'Media Picker', + stateSelector: 'img', + onclick: function () { + + var selectedElm = editor.selection.getNode(), + currentTarget; + + + if (selectedElm.nodeName === 'IMG') { + var img = $(selectedElm); + + var hasUdi = img.attr("data-udi") ? true : false; + + currentTarget = { + altText: img.attr("alt"), + url: img.attr("src") + }; + + if (hasUdi) { + currentTarget["udi"] = img.attr("data-udi"); + } else { + currentTarget["id"] = img.attr("rel"); + } + } + + userService.getCurrentUser().then(function (userData) { + if (callback) { + callback(currentTarget, userData); + } + }); + + } + }); + }, + + insertMediaInEditor: function (editor, img) { + if (img) { + + var hasUdi = img.udi ? true : false; + + var data = { + alt: img.altText || "", + src: (img.url) ? img.url : "nothing.jpg", + id: '__mcenew' + }; + + if (hasUdi) { + data["data-udi"] = img.udi; + } else { + //Considering these fixed because UDI will now be used and thus + // we have no need for rel http://issues.umbraco.org/issue/U4-6228, http://issues.umbraco.org/issue/U4-6595 + data["rel"] = img.id; + data["data-id"] = img.id; + } + + editor.insertContent(editor.dom.createHTML('img', data)); + + $timeout(function () { + var imgElm = editor.dom.get('__mcenew'); + var size = editor.dom.getSize(imgElm); + + if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) { + var newSize = imageHelper.scaleToMaxSize(editor.settings.maxImageSize, size.w, size.h); + + var s = "width: " + newSize.width + "px; height:" + newSize.height + "px;"; + editor.dom.setAttrib(imgElm, 'style', s); + editor.dom.setAttrib(imgElm, 'id', null); + + if (img.url) { + var src = img.url + "?width=" + newSize.width + "&height=" + newSize.height; + editor.dom.setAttrib(imgElm, 'data-mce-src', src); + } + } + }, 500); + } + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createUmbracoMacro + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the insert umbrco macro tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + * @param {Object} $scope the current controller scope + */ + createInsertMacro: function (editor, $scope, callback) { + + var createInsertMacroScope = this; + + /** Adds custom rules for the macro plugin and custom serialization */ + editor.on('preInit', function (args) { + //this is requires so that we tell the serializer that a 'div' is actually allowed in the root, otherwise the cleanup will strip it out + editor.serializer.addRules('div'); + + /** This checks if the div is a macro container, if so, checks if its wrapped in a p tag and then unwraps it (removes p tag) */ + editor.serializer.addNodeFilter('div', function (nodes, name) { + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].attr("class") === "umb-macro-holder" && nodes[i].parent && nodes[i].parent.name.toUpperCase() === "P") { + nodes[i].parent.unwrap(); + } + } + }); + + }); + + /** + * Because the macro gets wrapped in a P tag because of the way 'enter' works, this + * method will return the macro element if not wrapped in a p, or the p if the macro + * element is the only one inside of it even if we are deep inside an element inside the macro + */ + function getRealMacroElem(element) { + var e = $(element).closest(".umb-macro-holder"); + if (e.length > 0) { + if (e.get(0).parentNode.nodeName === "P") { + //now check if we're the only element + if (element.parentNode.childNodes.length === 1) { + return e.get(0).parentNode; + } + } + return e.get(0); + } + return null; + } + + /** Adds the button instance */ + editor.addButton('umbmacro', { + icon: 'custom icon-settings-alt', + tooltip: 'Insert macro', + onPostRender: function () { + + var ctrl = this; + var isOnMacroElement = false; + + /** + if the selection comes from a different element that is not the macro's + we need to check if the selection includes part of the macro, if so we'll force the selection + to clear to the next element since if people can select part of the macro markup they can then modify it. + */ + function handleSelectionChange() { + + if (!editor.selection.isCollapsed()) { + var endSelection = tinymce.activeEditor.selection.getEnd(); + var startSelection = tinymce.activeEditor.selection.getStart(); + //don't proceed if it's an entire element selected + if (endSelection !== startSelection) { + + //if the end selection is a macro then move the cursor + //NOTE: we don't have to handle when the selection comes from a previous parent because + // that is automatically taken care of with the normal onNodeChanged logic since the + // evt.element will be the macro once it becomes part of the selection. + var $testForMacro = $(endSelection).closest(".umb-macro-holder"); + if ($testForMacro.length > 0) { + + //it came from before so move after, if there is no after then select ourselves + var next = $testForMacro.next(); + if (next.length > 0) { + editor.selection.setCursorLocation($testForMacro.next().get(0)); + } else { + selectMacroElement($testForMacro.get(0)); + } + + } + } + } + } + + /** helper method to select the macro element */ + function selectMacroElement(macroElement) { + + // move selection to top element to ensure we can't edit this + editor.selection.select(macroElement); + + // check if the current selection *is* the element (ie bug) + var currentSelection = editor.selection.getStart(); + if (tinymce.isIE) { + if (!editor.dom.hasClass(currentSelection, 'umb-macro-holder')) { + while (!editor.dom.hasClass(currentSelection, 'umb-macro-holder') && currentSelection.parentNode) { + currentSelection = currentSelection.parentNode; + } + editor.selection.select(currentSelection); + } + } + } + + /** + * Add a node change handler, test if we're editing a macro and select the whole thing, then set our isOnMacroElement flag. + * If we change the selection inside this method, then we end up in an infinite loop, so we have to remove ourselves + * from the event listener before changing selection, however, it seems that putting a break point in this method + * will always cause an 'infinite' loop as the caret keeps changing. + */ + function onNodeChanged(evt) { + + //set our macro button active when on a node of class umb-macro-holder + var $macroElement = $(evt.element).closest(".umb-macro-holder"); + + handleSelectionChange(); + + //set the button active + ctrl.active($macroElement.length !== 0); + + if ($macroElement.length > 0) { + var macroElement = $macroElement.get(0); + + //remove the event listener before re-selecting + editor.off('NodeChange', onNodeChanged); + + selectMacroElement(macroElement); + + //set the flag + isOnMacroElement = true; + + //re-add the event listener + editor.on('NodeChange', onNodeChanged); + } else { + isOnMacroElement = false; + } + + } + + /** when the contents load we need to find any macros declared and load in their content */ + editor.on("LoadContent", function (o) { + + //get all macro divs and load their content + $(editor.dom.select(".umb-macro-holder.mceNonEditable")).each(function () { + createInsertMacroScope.loadMacroContent($(this), null, $scope); + }); + + }); + + /** This prevents any other commands from executing when the current element is the macro so the content cannot be edited */ + editor.on('BeforeExecCommand', function (o) { + if (isOnMacroElement) { + if (o.preventDefault) { + o.preventDefault(); + } + if (o.stopImmediatePropagation) { + o.stopImmediatePropagation(); + } + return; + } + }); + + /** This double checks and ensures you can't paste content into the rendered macro */ + editor.on("Paste", function (o) { + if (isOnMacroElement) { + if (o.preventDefault) { + o.preventDefault(); + } + if (o.stopImmediatePropagation) { + o.stopImmediatePropagation(); + } + return; + } + }); + + //set onNodeChanged event listener + editor.on('NodeChange', onNodeChanged); + + /** + * Listen for the keydown in the editor, we'll check if we are currently on a macro element, if so + * we'll check if the key down is a supported key which requires an action, otherwise we ignore the request + * so the macro cannot be edited. + */ + editor.on('KeyDown', function (e) { + if (isOnMacroElement) { + var macroElement = editor.selection.getNode(); + + //get the 'real' element (either p or the real one) + macroElement = getRealMacroElem(macroElement); + + //prevent editing + e.preventDefault(); + e.stopPropagation(); + + var moveSibling = function (element, isNext) { + var $e = $(element); + var $sibling = isNext ? $e.next() : $e.prev(); + if ($sibling.length > 0) { + editor.selection.select($sibling.get(0)); + editor.selection.collapse(true); + } else { + //if we're moving previous and there is no sibling, then lets recurse and just select the next one + if (!isNext) { + moveSibling(element, true); + return; + } + + //if there is no sibling we'll generate a new p at the end and select it + editor.setContent(editor.getContent() + "

 

"); + editor.selection.select($(editor.dom.getRoot()).children().last().get(0)); + editor.selection.collapse(true); + + } + }; + + //supported keys to move to the next or prev element (13-enter, 27-esc, 38-up, 40-down, 39-right, 37-left) + //supported keys to remove the macro (8-backspace, 46-delete) + //TODO: Should we make the enter key insert a line break before or leave it as moving to the next element? + if ($.inArray(e.keyCode, [13, 40, 39]) !== -1) { + //move to next element + moveSibling(macroElement, true); + } else if ($.inArray(e.keyCode, [27, 38, 37]) !== -1) { + //move to prev element + moveSibling(macroElement, false); + } else if ($.inArray(e.keyCode, [8, 46]) !== -1) { + //delete macro element + + //move first, then delete + moveSibling(macroElement, false); + editor.dom.remove(macroElement); + } + return; + } + }); + + }, + + /** The insert macro button click event handler */ + onclick: function () { + + var dialogData = { + //flag for use in rte so we only show macros flagged for the editor + richTextEditor: true + }; + + //when we click we could have a macro already selected and in that case we'll want to edit the current parameters + //so we'll need to extract them and submit them to the dialog. + var macroElement = editor.selection.getNode(); + macroElement = getRealMacroElem(macroElement); + if (macroElement) { + //we have a macro selected so we'll need to parse it's alias and parameters + var contents = $(macroElement).contents(); + var comment = _.find(contents, function (item) { + return item.nodeType === 8; + }); + if (!comment) { + throw "Cannot parse the current macro, the syntax in the editor is invalid"; + } + var syntax = comment.textContent.trim(); + var parsed = macroService.parseMacroSyntax(syntax); + dialogData = { + macroData: parsed + }; + } + + if (callback) { + callback(dialogData); + } + + } + }); + }, + + insertMacroInEditor: function (editor, macroObject, $scope) { + + //put the macro syntax in comments, we will parse this out on the server side to be used + //for persisting. + var macroSyntaxComment = ""; + //create an id class for this element so we can re-select it after inserting + var uniqueId = "umb-macro-" + editor.dom.uniqueId(); + var macroDiv = editor.dom.create('div', { + 'class': 'umb-macro-holder ' + macroObject.macroAlias + ' mceNonEditable ' + uniqueId + }, + macroSyntaxComment + 'Macro alias: ' + macroObject.macroAlias + ''); + + editor.selection.setNode(macroDiv); + + var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId)); + + //async load the macro content + this.loadMacroContent($macroDiv, macroObject, $scope); + + }, + + /** loads in the macro content async from the server */ + loadMacroContent: function ($macroDiv, macroData, $scope) { + + //if we don't have the macroData, then we'll need to parse it from the macro div + if (!macroData) { + var contents = $macroDiv.contents(); + var comment = _.find(contents, function (item) { + return item.nodeType === 8; + }); + if (!comment) { + throw "Cannot parse the current macro, the syntax in the editor is invalid"; + } + var syntax = comment.textContent.trim(); + var parsed = macroService.parseMacroSyntax(syntax); + macroData = parsed; + } + + var $ins = $macroDiv.find("ins"); + + //show the throbber + $macroDiv.addClass("loading"); + + var contentId = $routeParams.id; + + //need to wrap in safe apply since this might be occuring outside of angular + angularHelper.safeApply($scope, function () { + macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary) + .then(function (htmlResult) { + + $macroDiv.removeClass("loading"); + htmlResult = htmlResult.trim(); + if (htmlResult !== "") { + $ins.html(htmlResult); + } + }); + }); + + }, + + createLinkPicker: function (editor, $scope, onClick) { + + function createLinkList(callback) { + return function () { + var linkList = editor.settings.link_list; + + if (typeof (linkList) === "string") { + tinymce.util.XHR.send({ + url: linkList, + success: function (text) { + callback(tinymce.util.JSON.parse(text)); + } + }); + } else { + callback(linkList); + } + }; + } + + function showDialog(linkList) { + var data = {}, + selection = editor.selection, + dom = editor.dom, + selectedElm, anchorElm, initialText; + var win, linkListCtrl, relListCtrl, targetListCtrl; + + function linkListChangeHandler(e) { + var textCtrl = win.find('#text'); + + if (!textCtrl.value() || (e.lastControl && textCtrl.value() === e.lastControl.text())) { + textCtrl.value(e.control.text()); + } + + win.find('#href').value(e.control.value()); + } + + function buildLinkList() { + var linkListItems = [{ + text: 'None', + value: '' }]; - tinymce.each(linkList, function(link) { - linkListItems.push({ - text: link.text || link.title, - value: link.value || link.url, - menu: link.menu - }); - }); + tinymce.each(linkList, function (link) { + linkListItems.push({ + text: link.text || link.title, + value: link.value || link.url, + menu: link.menu + }); + }); - return linkListItems; - } + return linkListItems; + } - function buildRelList(relValue) { - var relListItems = [{ - text: 'None', - value: '' + function buildRelList(relValue) { + var relListItems = [{ + text: 'None', + value: '' }]; - tinymce.each(editor.settings.rel_list, function(rel) { - relListItems.push({ - text: rel.text || rel.title, - value: rel.value, - selected: relValue === rel.value - }); - }); + tinymce.each(editor.settings.rel_list, function (rel) { + relListItems.push({ + text: rel.text || rel.title, + value: rel.value, + selected: relValue === rel.value + }); + }); - return relListItems; - } + return relListItems; + } - function buildTargetList(targetValue) { - var targetListItems = [{ - text: 'None', - value: '' + function buildTargetList(targetValue) { + var targetListItems = [{ + text: 'None', + value: '' }]; - if (!editor.settings.target_list) { - targetListItems.push({ - text: 'New window', - value: '_blank' - }); - } + if (!editor.settings.target_list) { + targetListItems.push({ + text: 'New window', + value: '_blank' + }); + } - tinymce.each(editor.settings.target_list, function(target) { - targetListItems.push({ - text: target.text || target.title, - value: target.value, - selected: targetValue === target.value - }); - }); + tinymce.each(editor.settings.target_list, function (target) { + targetListItems.push({ + text: target.text || target.title, + value: target.value, + selected: targetValue === target.value + }); + }); - return targetListItems; - } + return targetListItems; + } - function buildAnchorListControl(url) { - var anchorList = []; + function buildAnchorListControl(url) { + var anchorList = []; - tinymce.each(editor.dom.select('a:not([href])'), function(anchor) { - var id = anchor.name || anchor.id; + tinymce.each(editor.dom.select('a:not([href])'), function (anchor) { + var id = anchor.name || anchor.id; - if (id) { - anchorList.push({ - text: id, - value: '#' + id, - selected: url.indexOf('#' + id) !== -1 - }); - } - }); + if (id) { + anchorList.push({ + text: id, + value: '#' + id, + selected: url.indexOf('#' + id) !== -1 + }); + } + }); - if (anchorList.length) { - anchorList.unshift({ - text: 'None', - value: '' - }); + if (anchorList.length) { + anchorList.unshift({ + text: 'None', + value: '' + }); - return { - name: 'anchor', - type: 'listbox', - label: 'Anchors', - values: anchorList, - onselect: linkListChangeHandler - }; - } - } + return { + name: 'anchor', + type: 'listbox', + label: 'Anchors', + values: anchorList, + onselect: linkListChangeHandler + }; + } + } - function updateText() { - if (!initialText && data.text.length === 0) { - this.parent().parent().find('#text')[0].value(this.value()); - } - } + function updateText() { + if (!initialText && data.text.length === 0) { + this.parent().parent().find('#text')[0].value(this.value()); + } + } - selectedElm = selection.getNode(); - anchorElm = dom.getParent(selectedElm, 'a[href]'); + selectedElm = selection.getNode(); + anchorElm = dom.getParent(selectedElm, 'a[href]'); - data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({format: 'text'}); - data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : ''; - data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : ''; - data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : ''; + data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({ + format: 'text' + }); + data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : ''; + data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : ''; + data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : ''; - if (selectedElm.nodeName === "IMG") { - data.text = initialText = " "; - } + if (selectedElm.nodeName === "IMG") { + data.text = initialText = " "; + } - if (linkList) { - linkListCtrl = { - type: 'listbox', - label: 'Link list', - values: buildLinkList(), - onselect: linkListChangeHandler - }; - } + if (linkList) { + linkListCtrl = { + type: 'listbox', + label: 'Link list', + values: buildLinkList(), + onselect: linkListChangeHandler + }; + } - if (editor.settings.target_list !== false) { - targetListCtrl = { - name: 'target', - type: 'listbox', - label: 'Target', - values: buildTargetList(data.target) - }; - } + if (editor.settings.target_list !== false) { + targetListCtrl = { + name: 'target', + type: 'listbox', + label: 'Target', + values: buildTargetList(data.target) + }; + } - if (editor.settings.rel_list) { - relListCtrl = { - name: 'rel', - type: 'listbox', - label: 'Rel', - values: buildRelList(data.rel) - }; - } + if (editor.settings.rel_list) { + relListCtrl = { + name: 'rel', + type: 'listbox', + label: 'Rel', + values: buildRelList(data.rel) + }; + } - var injector = angular.element(document.getElementById("umbracoMainPageBody")).injector(); - var dialogService = injector.get("dialogService"); - var currentTarget = null; + var currentTarget = null; - //if we already have a link selected, we want to pass that data over to the dialog - if(anchorElm){ - var anchor = $(anchorElm); - currentTarget = { - name: anchor.attr("title"), - url: anchor.attr("href"), - target: anchor.attr("target") - }; + //if we already have a link selected, we want to pass that data over to the dialog + if (anchorElm) { + var anchor = $(anchorElm); + currentTarget = { + name: anchor.attr("title"), + url: anchor.attr("href"), + target: anchor.attr("target") + }; - //locallink detection, we do this here, to avoid poluting the dialogservice - //so the dialog service can just expect to get a node-like structure - if (currentTarget.url.indexOf("localLink:") > 0) { - var linkId = currentTarget.url.substring(currentTarget.url.indexOf(":") + 1, currentTarget.url.length - 1); - //we need to check if this is an INT or a UDI - var parsedIntId = parseInt(linkId, 10); - if (isNaN(parsedIntId)) { - //it's a UDI - currentTarget.udi = linkId; - } - else { - currentTarget.id = linkId; - } - } - } + // drop the lead char from the anchor text, if it has a value + var anchorVal = anchor[0].dataset.anchor; + if (anchorVal) { + currentTarget.anchor = anchorVal.substring(1); + } - if(onClick) { - onClick(currentTarget, anchorElm); - } + //locallink detection, we do this here, to avoid poluting the dialogservice + //so the dialog service can just expect to get a node-like structure + if (currentTarget.url.indexOf("localLink:") > 0) { + // if the current link has an anchor, it needs to be considered when getting the udi/id + // if an anchor exists, reduce the substring max by its length plus two to offset the removed prefix and trailing curly brace + var linkId = currentTarget.url.substring(currentTarget.url.indexOf(":") + 1, currentTarget.url.lastIndexOf("}")); - } + //we need to check if this is an INT or a UDI + var parsedIntId = parseInt(linkId, 10); + if (isNaN(parsedIntId)) { + //it's a UDI + currentTarget.udi = linkId; + } else { + currentTarget.id = linkId; + } + } + } - editor.addButton('link', { - icon: 'link', - tooltip: 'Insert/edit link', - shortcut: 'Ctrl+K', - onclick: createLinkList(showDialog), - stateSelector: 'a[href]' - }); + if (onClick) { + onClick(currentTarget, anchorElm); + } - editor.addButton('unlink', { - icon: 'unlink', - tooltip: 'Remove link', - cmd: 'unlink', - stateSelector: 'a[href]' - }); + } - editor.addShortcut('Ctrl+K', '', createLinkList(showDialog)); - this.showDialog = showDialog; + editor.addButton('link', { + icon: 'link', + tooltip: 'Insert/edit link', + shortcut: 'Ctrl+K', + onclick: createLinkList(showDialog), + stateSelector: 'a[href]' + }); - editor.addMenuItem('link', { - icon: 'link', - text: 'Insert link', - shortcut: 'Ctrl+K', - onclick: createLinkList(showDialog), - stateSelector: 'a[href]', - context: 'insert', - prependToContext: true - }); + editor.addButton('unlink', { + icon: 'unlink', + tooltip: 'Remove link', + cmd: 'unlink', + stateSelector: 'a[href]' + }); - }, + editor.addShortcut('Ctrl+K', '', createLinkList(showDialog)); + this.showDialog = showDialog; - insertLinkInEditor: function(editor, target, anchorElm) { + editor.addMenuItem('link', { + icon: 'link', + text: 'Insert link', + shortcut: 'Ctrl+K', + onclick: createLinkList(showDialog), + stateSelector: 'a[href]', + context: 'insert', + prependToContext: true + }); - var href = target.url; - // We want to use the Udi. If it is set, we use it, else fallback to id, and finally to null - var hasUdi = target.udi ? true : false; - var id = hasUdi ? target.udi : (target.id ? target.id : null); + }, - //Create a json obj used to create the attributes for the tag - function createElemAttributes() { - var a = { - href: href, - title: target.name, - target: target.target ? target.target : null, - rel: target.rel ? target.rel : null - }; - if (hasUdi) { - a["data-udi"] = target.udi; - } - else if (target.id) { - a["data-id"] = target.id; - } - return a; - } + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#getAnchorNames + * @methodOf umbraco.services.tinyMceService + * + * @description + * From the given string, generates a string array where each item is the id attribute value from a named anchor + * 'some string with a named anchor' returns ['anchor'] + * + * @param {string} input the string to parse + */ + getAnchorNames: function (input) { + var anchorPattern = //gi; + var matches = input.match(anchorPattern); + var anchors = []; - function insertLink() { - if (anchorElm) { - editor.dom.setAttribs(anchorElm, createElemAttributes()); + if (matches) { + anchors = matches.map(function (v) { + return v.substring(v.indexOf('"') + 1, v.lastIndexOf('\\')); + }); + } - editor.selection.select(anchorElm); - editor.execCommand('mceEndTyping'); - } - else { - editor.execCommand('mceInsertLink', false, createElemAttributes()); - } - } + return anchors; + }, - if (!href) { - editor.execCommand('unlink'); - return; - } + insertLinkInEditor: function (editor, target, anchorElm) { - //if we have an id, it must be a locallink:id, aslong as the isMedia flag is not set - if(id && (angular.isUndefined(target.isMedia) || !target.isMedia)){ - - href = "/{localLink:" + id + "}"; + var href = target.url; + // We want to use the Udi. If it is set, we use it, else fallback to id, and finally to null + var hasUdi = target.udi ? true : false; + var id = hasUdi ? target.udi : (target.id ? target.id : null); - insertLink(); - return; - } + // if an anchor exists, check that it is appropriately prefixed + if (target.anchor && target.anchor[0] !== '?' && target.anchor[0] !== '#') { + target.anchor = (target.anchor.indexOf('=') === -1 ? '#' : '?') + target.anchor; + } + + // the href might be an external url, so check the value for an anchor/qs + // href has the anchor re-appended later, hence the reset here to avoid duplicating the anchor + if (!target.anchor) { + var urlParts = href.split(/(#|\?)/); + if (urlParts.length === 3) { + href = urlParts[0]; + target.anchor = urlParts[1] + urlParts[2]; + } + } + + //Create a json obj used to create the attributes for the tag + function createElemAttributes() { + var a = { + href: href, + title: target.name, + target: target.target ? target.target : null, + rel: target.rel ? target.rel : null + }; - // Is email and not //user@domain.com - if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf('mailto:') === -1) { - href = 'mailto:' + href; - insertLink(); - return; - } + if (hasUdi) { + a["data-udi"] = target.udi; + } else if (target.id) { + a["data-id"] = target.id; + } - // Is www. prefixed - if (/^\s*www\./i.test(href)) { - href = 'http://' + href; - insertLink(); - return; - } + if (target.anchor) { + a["data-anchor"] = target.anchor; + a.href = a.href + target.anchor; + } else { + a["data-anchor"] = null; + } - insertLink(); + return a; + } - } + function insertLink() { + if (anchorElm) { + editor.dom.setAttribs(anchorElm, createElemAttributes()); - }; + editor.selection.select(anchorElm); + editor.execCommand('mceEndTyping'); + } else { + editor.execCommand('mceInsertLink', false, createElemAttributes()); + } + } + + if (!href) { + editor.execCommand('unlink'); + return; + } + + //if we have an id, it must be a locallink:id, aslong as the isMedia flag is not set + if (id && (angular.isUndefined(target.isMedia) || !target.isMedia)) { + + href = "/{localLink:" + id + "}"; + + insertLink(); + return; + } + + // Is email and not //user@domain.com + if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf('mailto:') === -1) { + href = 'mailto:' + href; + insertLink(); + return; + } + + // Is www. prefixed + if (/^\s*www\./i.test(href)) { + href = 'http://' + href; + insertLink(); + return; + } + + insertLink(); + + } + + }; } angular.module('umbraco.services').factory('tinyMceService', tinyMceService); diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index ffd363fe31..2eb2735279 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -531,6 +531,13 @@ input.umb-group-builder__group-title-input { overflow: hidden; } + .editor-validation-pattern{ + border: 1px solid @gray-7; + margin: 10px 0 0; + padding: 6px; + max-height: 32px; + } + .umb-dropdown { width: 100%; } 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 47fabb7573..67b684ab7e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -3,10 +3,24 @@ // -------------------------------------------------- .umb-property-editor { min-width:66.6%; + + &-pull { + float:left; + width:66.6%; + } + + &-push { + float:right; + } } .umb-property-editor-tiny { width: 60px; + + &.umb-editor-push { + width:30%; + min-width:0; + } } .umb-property-editor-small { @@ -28,6 +42,27 @@ width: 99%; } +// displays property inline with preceeding +.umb-property { + &--pull { + float:left; + width:60%; + } + + &--push { + float:right; + width:35%; + } + + &--pull, &--push { + .umb-editor { + min-width:0; + width:100%; + } + } +} + + // // Content picker // -------------------------------------------------- diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js index be65dffd48..2d8ad46371 100644 --- a/src/Umbraco.Web.UI.Client/src/routes.js +++ b/src/Umbraco.Web.UI.Client/src/routes.js @@ -147,13 +147,32 @@ app.config(function ($routeProvider) { resolve: canRoute(true) }) .when('/:section/:tree/:method?', { - templateUrl: function (rp) { + //This allows us to dynamically change the template for this route since you cannot inject services into the templateUrl method. + template: "
", + //This controller will execute for this route, then we replace the template dynamnically based on the current tree. + controller: function ($scope, $route, $routeParams, treeService) { - //if there is no method registered for this then show the dashboard - if (!rp.method) - return "views/common/dashboard.html"; - - return ('views/' + rp.tree + '/' + rp.method + '.html'); + if (!$routeParams.method) { + $scope.templateUrl = "views/common/dashboard.html"; + } + + // Here we need to figure out if this route is for a package tree and if so then we need + // to change it's convention view path to: + // /App_Plugins/{mypackage}/backoffice/{treetype}/{method}.html + + // otherwise if it is a core tree we use the core paths: + // views/{treetype}/{method}.html + + var packageTreeFolder = treeService.getTreePackageFolder($routeParams.tree); + + if (packageTreeFolder) { + $scope.templateUrl = (Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + + "/" + packageTreeFolder + + "/backoffice/" + $routeParams.tree + "/" + $routeParams.method + ".html"); + } + else { + $scope.templateUrl = ('views/' + $routeParams.tree + '/' + $routeParams.method + '.html'); + } }, reloadOnSearch: false, resolve: canRoute(true) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js index 9bbf38568f..720edc2114 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -1,155 +1,170 @@ //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", - function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService) { - var dialogOptions = $scope.model; + function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService, tinyMceService) { + var dialogOptions = $scope.model; - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); + var anchorPattern = /
/gi; - if(!$scope.model.title) { - localizationService.localize("defaultdialogs_selectLink").then(function(value){ - $scope.model.title = value; - }); - } + var searchText = "Search..."; + localizationService.localize("general_search").then(function (value) { + searchText = value + "..."; + }); - $scope.dialogTreeApi = {}; - $scope.model.target = {}; - $scope.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - }; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectLink"); + } - $scope.showTarget = $scope.model.hideTarget !== true; + $scope.dialogTreeApi = {}; + $scope.model.target = {}; + $scope.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; - if (dialogOptions.currentTarget) { - $scope.model.target = dialogOptions.currentTarget; + $scope.showTarget = $scope.model.hideTarget !== true; - //if we have a node ID, we fetch the current node to build the form data - if ($scope.model.target.id || $scope.model.target.udi) { + if (dialogOptions.currentTarget) { + $scope.model.target = dialogOptions.currentTarget; + //if we have a node ID, we fetch the current node to build the form data + if ($scope.model.target.id || $scope.model.target.udi) { //will be either a udi or an int var id = $scope.model.target.udi ? $scope.model.target.udi : $scope.model.target.id; if (!$scope.model.target.path) { - + entityResource.getPath(id, "Document").then(function (path) { - $scope.model.target.path = path; - //now sync the tree to this path - $scope.dialogTreeApi.syncTree({ path: $scope.model.target.path, tree: "content" }); - }); - } + $scope.model.target.path = path; + //now sync the tree to this path + $scope.dialogTreeApi.syncTree({ + path: $scope.model.target.path, + tree: "content" + }); + }); + } - contentResource.getNiceUrl(id).then(function (url) { - $scope.model.target.url = url; - }); - } - } + // if a link exists, get the properties to build the anchor name list + contentResource.getById(id).then(function (resp) { + $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); + $scope.model.target.url = resp.urls[0]; + }); + } else if ($scope.model.target.url.length) { + // a url but no id/udi indicates an external link - trim the url to remove the anchor/qs + $scope.model.target.url = $scope.model.target.url.substring(0, $scope.model.target.url.search(/(#|\?)/)); + } + } else if (dialogOptions.anchors) { + $scope.anchorValues = dialogOptions.anchors; + } - function nodeSelectHandler(args) { + function nodeSelectHandler(args) { + if (args && args.event) { + args.event.preventDefault(); + args.event.stopPropagation(); + } - if(args && args.event) { - args.event.preventDefault(); - args.event.stopPropagation(); - } + eventsService.emit("dialogs.linkPicker.select", args); - eventsService.emit("dialogs.linkPicker.select", args); + if ($scope.currentNode) { + //un-select if there's a current one selected + $scope.currentNode.selected = false; + } - if ($scope.currentNode) { - //un-select if there's a current one selected - $scope.currentNode.selected = false; - } + $scope.currentNode = args.node; + $scope.currentNode.selected = true; + $scope.model.target.id = args.node.id; + $scope.model.target.udi = args.node.udi; + $scope.model.target.name = args.node.name; - $scope.currentNode = args.node; - $scope.currentNode.selected = true; - $scope.model.target.id = args.node.id; - $scope.model.target.udi = args.node.udi; - $scope.model.target.name = args.node.name; + if (args.node.id < 0) { + $scope.model.target.url = "/"; + } else { + contentResource.getById(args.node.id).then(function (resp) { + $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); + $scope.model.target.url = resp.urls[0]; + }); + } - if (args.node.id < 0) { - $scope.model.target.url = "/"; - } - else { - contentResource.getNiceUrl(args.node.id).then(function (url) { - $scope.model.target.url = url; - }); - } + if (!angular.isUndefined($scope.model.target.isMedia)) { + delete $scope.model.target.isMedia; + } + } - if (!angular.isUndefined($scope.model.target.isMedia)) { - delete $scope.model.target.isMedia; - } - } + function nodeExpandedHandler(args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + } - function nodeExpandedHandler(args) { - // open mini list view for list views - if (args.node.metaData.isContainer) { - openMiniListView(args.node); - } - } + $scope.switchToMediaPicker = function () { + userService.getCurrentUser().then(function (userData) { + $scope.mediaPickerOverlay = { + view: "mediapicker", + startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], + startNodeIsVirtual: userData.startMediaIds.length !== 1, + show: true, + submit: function (model) { + var media = model.selectedImages[0]; - $scope.switchToMediaPicker = function () { - userService.getCurrentUser().then(function (userData) { - $scope.mediaPickerOverlay = { - view: "mediapicker", - startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], - startNodeIsVirtual: userData.startMediaIds.length !== 1, - show: true, - submit: function(model) { - var media = model.selectedImages[0]; + $scope.model.target.id = media.id; + $scope.model.target.udi = media.udi; + $scope.model.target.isMedia = true; + $scope.model.target.name = media.name; + $scope.model.target.url = mediaHelper.resolveFile(media); - $scope.model.target.id = media.id; - $scope.model.target.udi = media.udi; - $scope.model.target.isMedia = true; - $scope.model.target.name = media.name; - $scope.model.target.url = mediaHelper.resolveFile(media); + debugger; - $scope.mediaPickerOverlay.show = false; - $scope.mediaPickerOverlay = null; - } - }; - }); - }; + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + } + }; + }); + }; - $scope.hideSearch = function () { - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = null; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = null; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + } - // method to select a search result - $scope.selectResult = function (evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, {event: evt, node: result}); - }; + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { + event: evt, + node: result + }); + }; //callback when there are search results - $scope.onSearchResults = function (results) { - $scope.searchInfo.results = results; + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; $scope.searchInfo.showSearch = true; - }; + }; $scope.onTreeInit = function () { $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); $scope.dialogTreeApi.callbacks.treeNodeExpanded(nodeExpandedHandler); } - - // Mini list view - $scope.selectListViewNode = function (node) { - node.selected = node.selected === true ? false : true; - nodeSelectHandler({}, { node: node }); - }; - $scope.closeMiniListView = function () { - $scope.miniListView = undefined; - }; + // Mini list view + $scope.selectListViewNode = function (node) { + node.selected = node.selected === true ? false : true; + nodeSelectHandler({}, { + node: node + }); + }; - function openMiniListView(node) { - $scope.miniListView = node; - } + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; - }); + function openMiniListView(node) { + $scope.miniListView = node; + } + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html index 7772479149..e4ab635a44 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html @@ -1,26 +1,41 @@
- + + localize="placeholder" + placeholder="@general_url" + class="umb-property-editor umb-textstring" + ng-model="model.target.url" + ng-disabled="model.target.id" /> + + + + + + + + + localize="placeholder" + placeholder="@placeholders_entername" + class="umb-property-editor umb-textstring" + ng-model="model.target.name" /> @@ -30,43 +45,39 @@
- + -
+
- +
- +
- +
@@ -80,11 +91,10 @@
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js index c48099c5fe..ca65e3d107 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js @@ -20,9 +20,10 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", } $scope.treeModel = { hideHeader: false - } + } + $scope.toggle = toggleHandler; userService.getCurrentUser().then(function (userData) { - $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; + $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; }); var node = dialogOptions.currentNode; @@ -57,7 +58,27 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", if (args.node.metaData.isContainer) { openMiniListView(args.node); } - } + } + + function toggleHandler(type){ + // If the relateToOriginal toggle is clicked + if(type === "relate"){ + if($scope.relateToOriginal){ + $scope.relateToOriginal = false; + return; + } + $scope.relateToOriginal = true; + } + + // If the recurvise toggle is clicked + if(type === "recursive"){ + if($scope.recursive){ + $scope.recursive = false; + return; + } + $scope.recursive = true; + } + } $scope.hideSearch = function () { $scope.searchInfo.showSearch = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/copy.html b/src/Umbraco.Web.UI.Client/src/views/content/copy.html index 7501508a48..018c543d08 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -69,13 +69,13 @@ - + - - + + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 0543fc0c66..78ce1f29a5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -88,7 +88,10 @@ function contentPickerController($scope, entityResource, editorState, iconHelper // sortable options $scope.sortableOptions = { + axis: "y", + containment: "parent", distance: 10, + opacity: 0.7, tolerance: "pointer", scroll: true, zIndex: 6000 diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js index 99c3fd80ff..397438d5a0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -1,7 +1,7 @@ (function() { "use strict"; - function GridRichTextEditorController($scope, tinyMceService, macroService) { + function GridRichTextEditorController($scope, tinyMceService, macroService, editorState) { var vm = this; @@ -11,9 +11,11 @@ vm.openEmbed = openEmbed; function openLinkPicker(editor, currentTarget, anchorElement) { + vm.linkPickerOverlay = { view: "linkpicker", currentTarget: currentTarget, + anchors: tinyMceService.getAnchorNames(JSON.stringify(editorState.current.properties)), show: true, submit: function(model) { tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index 54ec3031cb..a9d1cd1234 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -1,6 +1,6 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.RTEController", - function ($rootScope, $scope, $q, $locale, dialogService, $log, imageHelper, assetsService, $timeout, tinyMceService, angularHelper, stylesheetResource, macroService) { + function ($rootScope, $scope, $q, $locale, dialogService, $log, imageHelper, assetsService, $timeout, tinyMceService, angularHelper, stylesheetResource, macroService, editorState) { $scope.isLoading = true; @@ -273,11 +273,12 @@ angular.module("umbraco") syncContent(editor); }); - + tinyMceService.createLinkPicker(editor, $scope, function(currentTarget, anchorElement) { $scope.linkPickerOverlay = { view: "linkpicker", currentTarget: currentTarget, + anchors: tinyMceService.getAnchorNames(JSON.stringify(editorState.current.properties)), show: true, submit: function(model) { tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js index e92dd171c6..35b5bef306 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js @@ -5,15 +5,12 @@ angular.module("umbraco") var $typeahead; $scope.isLoading = true; - $scope.tagToAdd = ""; - - assetsService.loadJs("lib/typeahead.js/typeahead.bundle.min.js", $scope).then(function () { - - $scope.isLoading = false; - - //load current value - - if ($scope.model.value) { + $scope.tagToAdd = ""; + + function setModelValue(val) { + + $scope.model.value = val || $scope.model.value; + if ($scope.model.value) { if (!$scope.model.config.storageType || $scope.model.config.storageType !== "Json") { //it is csv if (!$scope.model.value) { @@ -21,7 +18,14 @@ angular.module("umbraco") } else { if($scope.model.value.length > 0) { - $scope.model.value = $scope.model.value.split(","); + // split the csv string, and remove any duplicate values + var tempArray = $scope.model.value.split(',').map(function(v) { + return v.trim(); + }); + + $scope.model.value = tempArray.filter(function(v, i, self) { + return self.indexOf(v) === i; + }); } } } @@ -29,6 +33,14 @@ angular.module("umbraco") else { $scope.model.value = []; } + } + + assetsService.loadJs("lib/typeahead.js/typeahead.bundle.min.js", $scope).then(function () { + + $scope.isLoading = false; + + //load current value + setModelValue(); // Method required by the valPropertyValidator directive (returns true if the property editor has at least one tag selected) $scope.validateMandatory = function () { @@ -85,17 +97,7 @@ angular.module("umbraco") //vice versa $scope.model.onValueChanged = function (newVal, oldVal) { //update the display val again if it has changed from the server - $scope.model.value = newVal; - - if (!$scope.model.config.storageType || $scope.model.config.storageType !== "Json") { - //it is csv - if (!$scope.model.value) { - $scope.model.value = []; - } - else { - $scope.model.value = $scope.model.value.split(","); - } - } + setModelValue(newVal); }; //configure the tags data source diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html index 313cf99a49..de232c690c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html @@ -8,7 +8,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js index 0f95f78adc..39ae1dd583 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js @@ -67,7 +67,7 @@ }); - vm.save = function () { + vm.save = function (suppressNotification) { vm.page.saveButtonState = "busy"; vm.template.content = vm.editor.getValue(); @@ -83,11 +83,13 @@ rebindCallback: function (orignal, saved) {} }).then(function (saved) { + if (!suppressNotification) { localizationService.localizeMany(["speechBubbles_templateSavedHeader", "speechBubbles_templateSavedText"]).then(function(data){ var header = data[0]; var message = data[1]; notificationsService.success(header, message); }); + } vm.page.saveButtonState = "success"; @@ -173,6 +175,21 @@ vm.page.loading = false; vm.template = template; + // if this is a new template, bind to the blur event on the name + if ($routeParams.create) { + $timeout(function() { + var nameField = angular.element(document.querySelector('[data-element="editor-name-field"]')); + if (nameField) { + nameField.bind('blur', function(event) { + if (event.target.value) { + vm.save(true); + } + }); + } + }); + } + + //sync state editorState.set(vm.template); navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true }).then(function (syncArgs) { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 48ccef0e51..a3f87ffc93 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -329,8 +329,10 @@ vm.unlockUserButtonState = "busy"; usersResource.unlockUsers([vm.user.id]).then(function (data) { vm.user.userState = 0; + vm.user.failedPasswordAttempts = 0; setUserDisplayState(); vm.unlockUserButtonState = "success"; + }, function (error) { vm.unlockUserButtonState = "error"; }); diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 382cb29d9f..ee18248997 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -344,6 +344,7 @@ Link title Link + Anchor / querystring Name Manage hostnames Close this window @@ -462,6 +463,7 @@ Enter your email... Enter a message... Your username is usually your email + #value or ?key=value Allow at root diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 89f9c7df00..e61c406fae 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -352,6 +352,7 @@ Link title Link + Anchor / querystring Name Close this window Are you sure you want to delete @@ -470,6 +471,7 @@ Enter your email... Enter a message... Your username is usually your email + #value or ?key=value Allow at root diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml index 6f8ef5292d..31d1f9392d 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml @@ -5,18 +5,21 @@ http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files - Gérer les noms d'hôtes + Culture et noms d'hôte Informations d'audit Parcourir Changer le type de document Copier Créer + Exporter + Créer un groupe Créer un package Supprimer Désactiver Vider la corbeille - Exporter un type - Importer un type + Activer + Exporter le type de document + Importer un type de document Importer un package Editer dans Canvas Déconnexion @@ -28,15 +31,48 @@ Rafraîchir Republier le site tout entier Récupérer + Spécifiez les permissions pour la page %0% + Choisissez où déplacer + dans l'arborescence ci-dessous Permissions Version antérieure Envoyer pour publication Envoyer pour traduction + Spécifier le groupe Trier Envoyer pour publication Traduire Mettre à jour - Valeur par défaut + Spécifier les permissions + Débloquer + Créer un modèle de contenu + Envoyer à nouveau l'invitation + + + Contenu + Administration + Structure + Autre + + + Permettre d'attribuer la culture et des noms d'hôte + Permettre d'accéder au journal d'historique d'un noeud + Permettre d'accéder à un noeud + Permettre de modifier le type de document d'un noeud + Permettre de copier un noeud + Permettre de créer des noeuds + Permettre de supprimer des noeuds + Permettre de déplacer un noeud + Permettre de définir et modifier l'accès public à un noeud + Permettre de publier un noeud + Permettre de modifier les permissions pour un noeud + Permettre de revenir à une situation antérieure + Permettre d'envoyer un noeud pour approbation avant publication + Permettre d'envoyer un noeud à la traduction + Permettre de modifier l'ordonnancement des noeuds + Permettre de traduire un noeud + Permettre de sauvegarder un noeud + Permettre la création d'un Modèle de Contenu Permission refusée. @@ -45,20 +81,24 @@ Noeud invalide. Domaine invalide. Domaine déjà assigné. - Domaine Langue + Domaine Nouveau domaine '%0%' créé Domaine '%0%' supprimé Domaine '%0%' déjà assigné -
Les domaines contenant un chemin d'un niveau sont autorisés, ex : "example.com/en". Pour autant, cela - devrait être évité. Utilisez plutôt la gestion des noms d'hôte.]]>
Domaine '%0%' mis à jour Editer les domaines actuels + +
Les domaines contenant un chemin d'un niveau sont autorisés, ex : "example.com/en". Pour autant, cela + devrait être évité. Utilisez plutôt la gestion de la culture et des noms d'hôte.]]> +
Hériter Culture - ou hériter de la culture des noeuds parents. S'appliquera aussi
- au noeud courant, à moins qu'un domaine ci-dessous soit aussi d'application.]]>
+ + ou hériter de la culture des noeuds parents. S'appliquera aussi
+ au noeud courant, à moins qu'un domaine ci-dessous soit aussi d'application.]]> +
Domaines @@ -85,10 +125,12 @@ Liste numérique Insérer une macro Insérer une image + Editer les relations Retourner à la liste Editer les relations Sauver Sauver et publier + Sauver et planifier Sauver et envoyer pour approbation Sauver la mise en page de la liste Prévisualiser @@ -97,7 +139,30 @@ Afficher les styles Insérer un tableau Générer les modèles - Sauver et générer les modèles + Sauver et générer les modèles + Défaire + Refaire + + + Aperçu pour + Suppressions de contenu réalisées par utilisateur + Dépublications réalisées par utilisateur + Sauvegardes et publications réalisées par utilisateur + Sauvegardes de contenu réalisées par utilisateur + Déplacements de contenu réalisés par utilisateur + Copies de contenu réalisées par utilisateur + Récupérations de contenu réalisées par utilisateur + Envois de contenu pour publication réalisés par utilisateur + Envois de contenu pour traduction réalisés par utilisateur + Copie + Publication + Déplacement + Sauvegarde + Suppression + Dépublication + Récupération + Envoi pour publication + Envoi pour traduction Pour changer le type de document du contenu séléctionné, faites d'abord un choix dans la liste des types valides à cet endroit. @@ -139,30 +204,38 @@ Dernière publication Il n'y a aucun élément à afficher Il n'y a aucun élément à afficher dans cette liste. + Aucun contenu n'a encore été ajouté + Aucun membre n'a encore été ajouté Type de Média Lien vers des média(s) Groupe de membres Rôle Type de membre + Aucune modification n'a été faite Aucune date choisie Titre de la page + Ce média n'a pas de lien Propriétés Ce document est publié mais n'est pas visible car son parent '%0%' n'est pas publié Oups : ce document est publié mais n'est pas présent dans le cache (erreur interne Umbraco) Oups: impossible d'obtenir cet url (erreur interne - voir fichier log) Oups: ce document est publié mais son url entrerait en collision avec le contenu %0% Publier + Publié + Publié (changements en cours) Statut de publication Publié le Dépublié le Supprimer la date - Ordre de tri mis à jour + Défininir la date + Ordre de tri mis à jour Pour trier les noeuds, faites-les simplement glisser à l'aide de la souris ou cliquez sur les entêtes de colonne. Vous pouvez séléctionner plusieurs noeuds en gardant la touche "shift" ou "ctrl" enfoncée pendant votre séléction. Statistiques Titre (optionnel) Texte alternatif (optionnel) Type Dépublier + Dépublié Dernière édition Date/heure à laquelle ce document a été édité Supprimer le(s) fichier(s) @@ -173,6 +246,22 @@ Cible Ceci se traduit par l'heure suivante sur le serveur : Qu'est-ce que cela signifie?]]> + Etes-vous certain(e) de vouloir supprimer cet éléménent? + La propriété %0% utilise l'éditeur %1% qui n'est pas supporté par Nested Content. + Ajouter un autre champ texte + Enlever ce champ texte + Racine du contenu + Cette valeur est masquée. Si vous avez besoin de pouvoir accéder à cette valeur, veuillez prendre contact avec l'administrateur du site web. + Cette valeur est masquée. + + + Créer un nouveau Modèle de Contenu à partir de '%0%' + Vide + Sélectionner un Modèle de Contenu + Modèle de Contenu créé + Un modèle de Contenu a été créé à partir de '%0%' + Un autre Modèle de Contenu existe déjà avec le même nom + Un Modèle de Contenu est du contenu pré-défini qu'un éditeur peut sélectionner et utiliser comme base pour la création de nouveau contenu Cliquez pour télécharger @@ -180,9 +269,10 @@ Lien vers le média ou cliquez ici pour choisir un fichier Les seuls types de fichiers autorisés sont - Impossible de télécharger ce fichier, il n'a pas un type de fichier autorisé. + Ce fichier ne peut pas ête chargé, il n'est pas d'un type de fichier autorisé. La taille maximum de fichier est - + Racine du média + Créer un nouveau membre Tous les membres @@ -190,12 +280,20 @@ Où voulez-vous créer le nouveau %0% Créer un élément sous + Sélectionnez le type de document pour lequel vous souhaitez créer un modèle de contenu Choisissez un type et un titre "Types de documents".]]> "Types de médias".]]> Type de document sans modèle Nouveau répertoire Nouveau type de données + Nouveau fichier javascript + Nouvelle vue partielle vide + Nouvelle macro pour vue partielle + Nouvelle vue partielle à partir d'un snippet + Nouvelle macro pour vue partielle vide + Nouvelle macro pour vue partielle à partir d'un snippet + Nouvelle macro pour vue partielle (sans macro) Parcourir votre site @@ -211,7 +309,8 @@ Invalider les changements Vous avez des changements en cours Etes-vous certain(e) de vouloir quitter cette page? - vous avez des changements en cours - + La dépublication va supprimer du site cette page ainsi que tous ses descendants. + Terminé @@ -270,7 +369,10 @@ Cette macro ne contient aucune propriété éditable Coller Editer les permissions pour - Les éléments dans la corbeille sont en cours de suppression. Ne fermez pas cette fenêtre avant que cette opération soit terminée. + Définir les permissions pour + Définir les permissions pour %0% pour le groupe d'utilisateurs %1% + Sélectionnez les groupes d'utilisateurs pour lesquels vous souhaitez définir les permissions + Les éléments dans la corbeille sont en cours de suppression. Ne fermez pas cette fenêtre avant que cette opération soit terminée. La corbeille est maintenant vide Les éléments supprimés de la corbeille seront supprimés définitivement regexlib.com rencontre actuellement des problèmes sur lesquels nous n'avons aucun contrôle. Nous sommes sincèrement désolés pour le désagrément.]]> @@ -282,28 +384,41 @@ Le cache du site va être mis à jour. Tous les contenus publiés seront mis à jour. Et tous les contenus dépubliés resteront invisibles. Nombre de colonnes Nombre de lignes - Définir un placeholder ID. En mettant un ID sur votre placeholder, vous pouvez injecter du contenu à cet endroit depuis les modèles enfants, - en faisant référence à cet ID au sein d'un élément <asp:content />.]]> - Séléctionnez un placeholder id dans la liste ci-dessous. Vous pouvez seulement - choisir un ID se trouvant dans le parent du modèle actuel.]]> + + Définir un placeholder ID. En mettant un ID sur votre placeholder, vous pouvez injecter du contenu à cet endroit depuis les modèles enfants, + en faisant référence à cet ID au sein d'un élément <asp:content />.]]> + + + Séléctionnez un placeholder ID dans la liste ci-dessous. Vous pouvez seulement + choisir un ID se trouvant dans le parent du modèle actuel.]]> + Cliquez sur l'image pour la voir en taille réelle Sélectionner un élément Voir l'élément de cache Créer un répertoire... Lier à l'original + Inclure les descendants La communauté la plus amicale Lier à la page Ouvre le document lié dans une nouvelle fenêtre ou un nouvel onglet Lier à un media + Lier à un fichier + Sélectionner le noeud de base du contenu Sélectionner le media Sélectionner l'icône Sélectionner l'élément Sélectionner le lien Sélectionner la macro Sélectionner le contenu + Sélectionner le noeud de base des media Sélectionner le membre Sélectionner le groupe de membres + Sélectionner le noeud + Sélectionner les sections + Sélectionner les utilisateurs + Aucune icone n'a été trouvée Il n'y a pas de paramètres pour cette macro + Il n'y a pas de macro disponible à insérer Fournisseurs externes d'identification Détails de l'exception Trace d'exécution @@ -312,12 +427,22 @@ Enlevez votre compte Sélectionner un éditeur + Selectionner un snippet - + %0%' ci-dessous.
Vous pouvez ajouter d'autres langues depuis le menu ci-dessous "Langues". - ]]>
+ ]]> + Nom de Culture + Modifiez la clé de l'élément de dictionaire. + + + + Aperçu du dictionaire Votre nom d'utilisateur @@ -325,15 +450,17 @@ Confirmation de votre mot de passe Nommer %0%... Entrez un nom... + Entrez un email... + Entrez un nom d'utilisateur... Libellé... Entrez une description... Rechercher... Filtrer... Ajouter des tags (appuyer sur enter entre chaque tag)... Entrez votre email - Votre nom d'utilisateur est généralement votre adresse email + Entrez un message... + Votre nom d'utilisateur est généralement votre adresse email - Autoriser à la racine Seuls les Types de Contenu qui ont ceci coché peuvent être créés au niveau racine des arborescences de contenu et de media @@ -352,6 +479,11 @@ Créer une liste personnalisée Supprimer la liste personnalisée + + Renommé + Entrez un nouveau nom de répertoire ici + %0% a été renommé en %1% + Ajouter une valeur de base Type de donnée en base de donées @@ -364,6 +496,13 @@ CSS associées Afficher le libellé Largeur et hauteur + Tous les types de propriétés & les données de propriétés + utilisant ce type de données seront supprimés définitivement, veuillez confirmer que vous voulez également les supprimer + Oui, supprimer + et tous les types de propriétés & les données de propriétés utilisant ce type de données + Sélectionnez le répertoire où déplacer + dans l'arborescence ci-dessous + a été déplacé sous Vos données ont été sauvegardées, mais avant de pouvoir publier votre page, il y a des erreurs que vous devez corriger : @@ -401,6 +540,7 @@ Il y a une erreur de configuration du type de données utilisé pour cette propriété, veuillez vérifier le type de données. + Options A propos Action Actions @@ -418,6 +558,7 @@ Fermer la fenêtre Commenter Confirmer + Conserver Conserver les proportions Continuer Copier @@ -429,6 +570,7 @@ Supprimé Suppression... Design + Dictionnaire Dimensions Bas Télécharger @@ -438,18 +580,26 @@ Email Erreur Trouver + Premier + Général + Groupes Hauteur Aide + Cacher + Historique Icône Importer + Info Marge intérieure Insérer Installer Non valide Justifier - Libellé + Libellé Langue + Dernier Mise en page + Liens En cours de chargement Bloqué Connexion @@ -457,6 +607,7 @@ Déconnexion Macro Obligatoire + Message Déplacer Plus Nom @@ -464,9 +615,12 @@ Suivant Non de + Inactif OK Ouvrir + Actif ou + Trier par Mot de passe Chemin Placeholder ID @@ -475,20 +629,25 @@ Propriétés Email de réception des données de formulaire Corbeille - Votre corbeille est vide + Votre corbeille est vide Restant + Enlever Renommer Renouveller Requis + Retrouver Réessayer Permissions + Publication Programmée Rechercher Désolé, nous ne pouvons pas trouver ce que vous recherchez + Aucun élément n'a été ajouté Serveur Montrer Afficher la page à l'envoi Taille Trier + Statut Envoyer Type Rechercher... @@ -515,16 +674,32 @@ Sauvegarde... actuel Intégrer + Retrouver sélectionné + Noir Vert Jaune Orange Bleu + Bleu-gris + Gris + Brun + Bleu Clair + Cyan + Vert Clair + Limon + Ambre + Orange Foncé Rouge + Rose + Mauve + Mauve Foncé + Indigo + Ajouter un onglet Ajouter une propriété @@ -542,6 +717,16 @@ Passer à la vue en liste Basculer vers l'autorisation comme racine + + Commenter/Décommenter les lignes + Supprimer la ligne + Copier les lignes vers le haut + Copier les lignes vers le bas + Déplacer les lignes vers le haut + Déplacer les lignes vers le bas + + Général + Editeur Couleur de fond @@ -558,30 +743,38 @@ Impossible de sauvegarder le fichier web.config. Veuillez modifier la "connection string" manuellement. Votre base de données a été détectée et est identifiée comme étant Configuration de la base de données - + installer pour installer la base de données Umbraco %0% - ]]> + ]]> + Suivant pour poursuivre.]]> - Base de données non trouvée ! Veuillez vérifier les informations de la "connection string" dans le fichier web.config.

+ + Base de données non trouvée ! Veuillez vérifier les informations de la "connection string" dans le fichier web.config.

Pour poursuivre, veuillez éditer le fichier "web.config" (avec Visual Studio ou votre éditeur de texte favori), scroller jusqu'en bas, ajouter le "connection string" pour votre base de données dans la ligne avec la clé "umbracoDbDSN" et sauvegarder le fichier.

Cliquez sur le bouton Réessayer lorsque cela est fait.
- Plus d'informations sur l'édition du fichier web.config ici.

]]>
- + Plus d'informations sur l'édition du fichier web.config ici.

]]> +
+ + Veuillez contacter votre fournisseur de services internet si nécessaire. - Si vous installez Umbraco sur un ordinateur ou un serveur local, vous aurez peut-être besoin de consulter votre administrateur système.]]> - + + + Appuyez sur le bouton Upgrader pour mettre à jour votre base de données vers Umbraco %0%

N'ayez pas d'inquiétude : aucun contenu ne sera supprimé et tout continuera à fonctionner parfaitement par après !

- ]]>
+ ]]> +
- Appuyez sur Suivant pour + Appuyez sur Suivant pour poursuivre. ]]> - + Suivant pour poursuivre la configuration]]> Le mot de passe par défaut doit être modifié !]]> L'utilisateur par défaut a été désactivé ou n'a pas accès à Umbraco!

Aucune autre action n'est requise. Cliquez sur Suivant pour poursuivre.]]> @@ -593,43 +786,56 @@ Fichiers et dossiers concernés Plus d'informations sur la configuration des permissions Vous devez donner à ASP.NET les droits de modification sur les fichiers/dossiers suivants - Vos configurations de permissions sont presque parfaites !

- Vous pouvez faire fonctionner Umbraco sans problèmes, mais vous ne serez pas en mesure d'installer des packages, ce qui est hautement recommandé pour tirer pleinement profit d'Umbraco.]]>
+ + Vos configurations de permissions sont presque parfaites !

+ Vous pouvez faire fonctionner Umbraco sans problèmes, mais vous ne serez pas en mesure d'installer des packages, ce qui est hautement recommandé pour tirer pleinement profit d'Umbraco.]]> +
Comment résoudre Cliquez ici pour lire la version texte tutoriel vidéo sur la définition des permissions des répertoires pour Umbraco, ou lisez la version texte.]]> - Vos configurations de permissions pourraient poser problème ! + + Vos configurations de permissions pourraient poser problème !

- Vous pouvez faire fonctionner Umbraco sans problèmes, mais vous ne serez pas en mesure d'installer des packages, ce qui est hautement recommandé pour tirer pleinement profit d'Umbraco.]]>
- Vos configurations de permissions ne sont pas prêtes pour Umbraco ! + Vous pouvez faire fonctionner Umbraco sans problèmes, mais vous ne serez pas en mesure d'installer des packages, ce qui est hautement recommandé pour tirer pleinement profit d'Umbraco.]]> + + + Vos configurations de permissions ne sont pas prêtes pour Umbraco !

- Pour faire fonctionner Umbraco, vous aurez besoin de mettre à jour les permissions sur les fichiers/dossiers.]]>
- Vos configurations de permissions sont parfaites !

- Vous êtes prêt(e) à faire fonctionner Umbraco et à installer des packages !]]>
+ Pour faire fonctionner Umbraco, vous aurez besoin de mettre à jour les permissions sur les fichiers/dossiers.]]> +
+ + Vos configurations de permissions sont parfaites !

+ Vous êtes prêt(e) à faire fonctionner Umbraco et à installer des packages !]]> +
Résoudre un problème sur un dossier Suivez ce lien pour plus d'informations sur les problèmes avec ASP.NET et la création de dossiers Définir les permissions de dossier - + + ]]> + Je veux démarrer "from scratch" - Apprenez comment) Vous pouvez toujours choisir d'installer Runway plus tard. Pour cela, allez dans la section "Développeur" et sélectionnez "Packages". - ]]> + ]]> + Vous venez de mettre en place une plateforme Umbraco toute nette. Que voulez-vous faire ensuite ? Runway est installé - + Voici la liste des modules recommandés, cochez ceux que vous souhaitez installer, ou regardez la liste complète des modules - ]]> + ]]> + Recommandé uniquement pour les utilisateurs expérimentés Je veux commencer avec un site simple - "Runway" est un site simple qui fournit des types de documents et des modèles de base. L'installateur peut mettre en place Runway automatiquement pour vous, mais vous pouvez facilement l'éditer, l'enrichir, ou le supprimer par la suite. Il n'est pas nécessaire, et vous pouvez parfaitement vous en passer pour utiliser Umbraco. Cela étant dit, @@ -640,7 +846,8 @@ Inclus avec Runway : Home page, Getting Started page, Installing Modules page.
Modules optionnels : Top Navigation, Sitemap, Contact, Gallery. - ]]>
+ ]]> + Qu'est-ce que Runway Etape 1/5 : Accepter la licence Etape 2/5 : Configuration de la base de données @@ -648,24 +855,36 @@ Etape 4/5 : Sécurité Umbraco Etape 5/5 : Umbraco est prêt Merci d'avoir choisi Umbraco - Parcourir votre nouveau site -Vous avez installé Runway, alors pourquoi ne pas jeter un oeil au look de votre nouveau site ?]]> - Aide et informations complémentaires -Obtenez de l'aide de notre "award winning" communauté, parcourez la documentation ou regardez quelques vidéos gratuites sur la manière de construire un site simple, d'utiliser les packages ainsi qu'un guide rapide sur la terminologie Umbraco]]> + + Parcourir votre nouveau site +Vous avez installé Runway, alors pourquoi ne pas jeter un oeil au look de votre nouveau site ?]]> + + + Aide et informations complémentaires +Obtenez de l'aide de notre communauté "award winning", parcourez la documentation ou regardez quelques vidéos gratuites sur la manière de construire un site simple, d'utiliser les packages ainsi qu'un guide rapide sur la terminologie Umbraco]]> + Umbraco %0% est installé et prêt à être utilisé - fichier /web.config et mettre à jour le paramètre AppSetting umbracoConfigurationStatus situé en bas à la valeur '%0%'.]]> - démarrer instantanément en cliquant sur le bouton "Lancer Umbraco" ci-dessous.
-Si vous débutez avec Umbraco, vous pouvez trouver une foule de ressources dans nos pages "Getting Started".]]>
- Lancer Umbraco -Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à ajouter du contenu, à mettre à jour les modèles d'affichage et feuilles de styles ou à ajouter de nouvelles fonctionnalités]]> + + fichier /web.config et mettre à jour le paramètre AppSetting umbracoConfigurationStatus situé en bas à la valeur '%0%'.]]> + + + démarrer instantanément en cliquant sur le bouton "Lancer Umbraco" ci-dessous.
+Si vous débutez avec Umbraco, vous pouvez trouver une foule de ressources dans nos pages "Getting Started".]]> +
+ + Lancer Umbraco +Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à ajouter du contenu, à mettre à jour les modèles d'affichage et feuilles de styles ou à ajouter de nouvelles fonctionnalités]]> + La connexion à la base de données a échoué. Umbraco Version 3 Umbraco Version 4 Regarder - Umbraco %0%, qu'il s'agisse d'une nouvelle installation ou d'une mise à jour à partir de la version 3.0 + + Umbraco %0%, qu'il s'agisse d'une nouvelle installation ou d'une mise à jour à partir de la version 3.0

- Appuyez sur "suivant" pour commencer l'assistant.]]>
+ Appuyez sur "suivant" pour commencer l'assistant.]]> +
Code de la Culture @@ -676,13 +895,13 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Renouvellez votre session maintenant pour sauvegarder votre travail - Joyeux dimanche - Joyeux lundi - Joyeux mardi - Joyeux mercredi + Joyeux dimanche détonnant + Joyeux lundi lumineux + Joyeux mardi magique + Joyeux mercredi merveilleux Joyeux jeudi Joyeux vendredi - Joyeux samedi + Joyeux chamedi Connectez-vous ci-dessous Identifiez-vous avec La session a expiré @@ -690,13 +909,95 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Mot de passe oublié? Un email contenant un lien pour ré-initialiser votre mot de passe sera envoyé à l'adresse spécifiée Un email contenant les instructions de ré-initialisation de votre mot de passe sera envoyée à l'adresse spécifiée si elle correspond à nos informations. + Montrer le mot de passe + Cacher le mot de passe Revenir au formulaire de connexion Veuillez fournir un nouveau mot de passe Votre mot de passe a été mis à jour Le lien sur lequel vous avez cliqué est non valide ou a expiré. Umbraco: Ré-initialiser le mot de passe - Votre nom d'utilisateur pour vous connecter au back-office Umbraco est : %0%.

Cliquez ici pour ré-initialiser votre mot de passe, ou recopiez cet URL dans votre navigateur :

%1%

]]> + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Une réinitialisation de votre mot de passe a été demandée +

+

+ Votre nom d'utilisateur pour vous connecter au back-office Umbraco est : %0% +

+

+ + + + + + +
+ + Cliquez sur ce lien pour réinitialiser votre mot de passe + +
+

+

Si vous ne pouvez pas cliquer sur le lien, recopiez cet URL dans votre navigateur :

+ + + + +
+ + %1% + +
+

+
+
+


+
+
+ + + ]]>
@@ -720,7 +1021,8 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Editez vos notifications pour %0% - + + ]]> + - Hello %0%

- -

Ceci est un email automatique pour vous informer que la tâche '%1%' - a été executée sur la page '%2%' - par l'utilisateur '%3%' -

- -

-

Résumé de la mise à jour :

- - %6% + + + + + + + +
+ + + +
+ + + + + +
+ +
+ +
+
+ + + + + +
+
+
+ + + + +
+ + + + +
+

+ Salut %0%, +

+

+ Ceci est un email automatique pour vous informer que la tâche '%1%' a été exécutée sur la page '%2%' par l'utilisateur '%3%' +

+ + + + + + +
+ +
+ MODIFIER
+
+

+

Résumé de la mise à jour :

+ + %6% +
+

+

+ Bonne journée !

+ Avec les salutations du Robot Umbraco +

+
+
+


+
+
-

- - - -

Bonne journée !

- Avec les salutations du Robot Umbraco -

]]>
+ + + ]]> + La notification [%0%] à propos de %1% a été executée sur %2% Notifications - + et localisez le package. Les packages Umbraco ont généralement une extension ".umb" ou ".zip". - ]]> + ]]> + + Déposez pour uploader + ou cliquez ici pour choisir les fichiers + Uploader un package + Installez un package local en le sélectionnant sur votre ordinateur. Installez uniquement des packages de sources fiables que vous connaissez + Uploader un autre package + Annuler et uploader un autre package + Licence + J'accepte + les conditions d'utilisation + Installer le package + Terminer + Packages installés + Vous n'avez aucun package installé + 'Packages' en haut à droite de votre écran]]> + Chercher des packages + Résultats pour + Nous n'avons rien pu trouver pour + Veuillez essayer de chercher un autre package ou naviguez à travers les catégories + Populaires + Nouvelles releases + a + points de karma + Information + Propriétaire + Contributeurs + Créé + Version actuelle + version .NET + Téléchargements + Coups de coeur + Compatibilité + Ce package est compatible avec les versions suivantes de Umbraco, selon les rapports des membres de la communauté. Une compatibilité complète ne peut pas être garantie pour les versions rapportées sous 100% + Sources externes Auteur Démo Documentation Meta data du package Nom du package Le package ne contient aucun élément -
- Vous pouvez supprimer tranquillement ce package de votre installation en cliquant sur "Désinstaller le package" ci-dessous.]]>
+ +
+ Vous pouvez supprimer tranquillement ce package de votre installation en cliquant sur "Désinstaller le package" ci-dessous.]]> +
Aucune mise à jour disponible Options du package Package readme @@ -785,9 +1177,11 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Le package a été désinstallé Le package a été désinstallé avec succès Désinstaller le package - + + Remarque : tous les documents, media etc. dépendant des éléments que vous supprimez vont cesser de fonctionner, ce qui peut provoquer une instabilité du système, - désinstallez donc avec prudence. En cas de doute, contactez l'auteur du package.]]> + désinstallez donc avec prudence. En cas de doute, contactez l'auteur du package.]]> + Télécharger la mise à jour depuis le repository Mettre à jour le package Instructions de mise à jour @@ -803,6 +1197,8 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Installation... Redémarrage, veuillez patienter... Terminé, votre navigateur va être rafraîchi, veuillez patienter... + Veuillez cliquer sur terminer pour compléter l'installation et recharger la page. + Package en cours de chargement... Coller en conservant le formatage (non recommandé) @@ -813,7 +1209,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Protection basée sur les rôles via les groupes de membres Umbraco.]]> - l'authentification basée sur les rôles.]]> + Vous devez créer un groupe avant de pouvoir utiliser l'authentification basée sur les rôles Page d'erreur Utilisé pour les personnes connectées, mais qui n'ont pas accès Choisissez comment restreindre l'accès à cette page @@ -834,32 +1230,51 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à %0% n'a pas pu être publié car cet élément est programmé pour être publié bientôt. ]]> - + - + + + - + + + - + + + + ]]> + Inclure les pages enfant non publiées Publication en cours - veuillez patienter... %0% pages sur %1% ont été publiées... %0% a été publié %0% et ses pages enfants ont été publiées Publier %0% et toutes ses pages enfant - Publier pour publier %0% et la rendre ainsi accessible publiquement.

+ + Publier pour publier %0% et la rendre ainsi accessible publiquement.

Vous pouvez publier cette page et toutes ses sous-pages en cochant Inclure les pages enfant non pubiées ci-dessous. - ]]>
+ ]]> +
Vous n'avez configuré aucune couleur approuvée - + + Vous avez choisi un élément de contenu actuellement supprimé ou dans la corbeille + Vous avez choisi des éléments de contenu actuellement supprimés ou dans la corbeille + + + Vous avez choisi un élément media actuellement supprimé ou dans la corbeille + Vous avez choisi des éléments media actuellement supprimés ou dans la corbeille + Elément supprimé + + introduire un lien externe choisir une page interne Légende @@ -870,6 +1285,10 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Réinitialiser + Définir le recadrage + Donnez un alias au recadrage ainsi que sa largeur et sa hauteur par défaut + Sauvegarder le recadrage + Ajouter un nouveau recadrage Version actuelle @@ -938,6 +1357,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Validation Les erreurs de validation doivent être corrigées avant de pouvoir sauvegarder l'élément Echec + Sauvegardé Permissions utilisateur insuffisantes, l'opération n'a pas pu être complétée Annulation L'opération a été annulée par une extension tierce @@ -971,6 +1391,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Erreur lors de la sauvegarde de l'utilisateur (consultez les logs) Utilisateur sauvegardé Type d'utilisateur sauvegardé + Groupe d'utilisateurs sauvegardé Fichier non sauvegardé Le fichier n'a pas pu être sauvegardé. Vérifiez les permissions de fichier. Fichier sauvegardé @@ -991,11 +1412,34 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Vue partielle sauvegardée sans erreurs ! Vue partielle non sauvegardée Une erreur est survenue lors de la sauvegarde du fichier. + Permissions sauvegardées pour Vue script sauvegardée Vue script sauvegardée sans erreur ! Vue script non sauvegardée Une erreur est survenue lors de la sauvegarde du fichier. Une erreur est survenue lors de la sauvegarde du fichier. + %0% groupes d'utilisateurs supprimés + %0% a été supprimé + %0% utilisateurs activés + Une erreur est survenue lors de l'activation des utilisateurs + %0% utilisateurs désactivés + Une erreur est survenue lors de la désactivation des utilisateurs + %0% est à présent activé + Une erreur est survenue lors de l'activation de l'utilisateur + %0% est à présent désactivé + Une erreur est survenue lors de la désactivation de l'utilisateur + Les groupes d'utilisateurs ont été définis + %0% groupes d'utilisateurs supprimés + %0% a été supprimé + %0% utilisateurs débloqués + Une erreur est survenue lors du débloquage des utilisateurs + %0% est à présent débloqué + Une erreur est survenue lors du débloquage de l'utilisateur + Le membre a été exporté vers le fichier + Une erreur est survenue lors de l'export du membre + L'utilisateur %0% a été supprimé + Inviter l'utilisateur + L'invitation a été envoyée à nouveau à %0% Utilise la syntaxe CSS. Ex : h1, .redHeader, .blueTex @@ -1005,19 +1449,115 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Prévisualiser Styles + Editer le modèle + + Sections Insérer une zone de contenu Insérer un placeholder de zone de contenu - Insérer un élément de dictionnaire - Insérer une Macro - Insérer un champ de la page Umbraco + + Insérer + Choisissez l'élément à insérer dans votre modèle + + Elément de dictionnaire + Un élément de dictionnaire est un espace pour un morceau de texte traduisible, ce qui facilite la création de designs pour des sites web multilangues. + + Macro + + Une Macro est un composant configurable, ce qui est génial pour les parties réutilisables de votre + design où vous devez pouvoir fournir des paramètres, + comme les galeries, les formulaires et les listes. + + + Valeur + Affiche la valeur d'un des champs de la page en cours, avec des options pour modifier la valeur ou spécifier des valeurs alternatives. + + Vue partielle + + Une vue partielle est un fichier modèle séparé qui peut être à l'intérieur d'un aute modèle, + c'est génial pour réutiliser du markup ou pour séparer des modèles complexes en plusieurs fichiers. + + Modèle de base - Guide rapide concernant les tags des modèles Umbraco + Pas de modèle de base + Pas de modèle + + Afficher un modèle enfant + + @RenderBody(). + ]]> + + + + Définir une section nommée + + @section { ... }. Celle-ci peut être affichée dans une région + spécifique du parent de ce modèle, en utilisant @RenderSection. + ]]> + + + Afficher une section nommée + + @RenderSection(name). + Ceci affiche une région d'un modèle enfant qui est entourée d'une définition @section [name]{ ... } correspondante. + ]]> + + + Nom de la section + La section est obligatoire + + Si obligatoire, le modèle enfant doit contenir une définition @section, sinon une erreur est affichée. + + + + Générateur de requêtes + Générer une requête + éléments trouvés, en + + Je veux + tout le contenu + le contenu du type "%0%" + à partir de + mon site web + + et + + est + n'est pas + avant + avant (incluant la date sélectionnée) + après + après (incluant la date sélectionnée) + égal + n'est pas égal + contient + ne contient pas + supérieur à + supérieur ou égal à + inférieur à + inférieur ou égal à + + Id + Nom + Date de Création + Date de Dernière Modification + + trier par + ascendant + descendant + Modèle + + - Rich Text Editor + Rich Text Editor Image Macro Embed @@ -1053,7 +1593,6 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Paramètres Configurez les paramètres qui peuvent être modifiés par les éditeurs - Styles Configurez les effets de style qui peuvent être modifiés par les éditeurs @@ -1061,13 +1600,16 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Autoriser tous les éditeurs Autoriser toutes les configurations de rangées + Eléments maximum + Laisser vide ou mettre à 0 pour un nombre illimté Configurer comme défaut Choisir en plus Choisir le défaut ont été ajoutés - - + + + Compositions Vous n'avez pas ajouté d'onglet Ajouter un nouvel onglet @@ -1081,6 +1623,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Modèles autorisés Sélectionnez les modèles que les éditeurs sont autorisés à utiliser pour du contenu de ce type. + Autorisé comme racine Autorisez les éditeurs à créer du contenu de ce type à la racine de l'arborescence de contenu. Oui - autoriser du contenu de ce type à la racine @@ -1089,6 +1632,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Autorisez la création de contenu des types spécifiés sous le contenu de ce type-ci Choisissez les noeuds enfants + Hériter des onglets et propriétés d'un type de document existant. De nouveaux onglets seront ajoutés au type de document actuel, ou fusionnés s'il existe un onglet avec un nom sililaire. Ce type de contenu est utilisé dans une composition, et ne peut donc pas être lui-même un composé. Il n'y a pas de type de contenu disponible à utiliser dans une composition. @@ -1122,40 +1666,57 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à utilisant cet éditeur seront mis à jour avec la nouvelle configuration Le membre peut éditer - Afficher dans le profil du membre - l'onglet n'a pas d'ordonnancement - - - - Création des modèles - ceci peut prendre un certain temps, ne vous inquiétez pas - Les modèles ont été générés - Les modèles n'ont pas pu être générés - La génération des modèles a échoué, veuillez consulter les erreurs dans le log Umbraco + Autoriser la modification de la valeur de cette propriété par le membre à partir de sa page de profil + Est une donnée sensible + Cacher cette propriété aux éditeurs de contenu qui n'ont pas accès à la visualisation des données sensibles + Afficher dans le profil du membre + Permettre d'afficher la valeur de cette propriété sur la page de profil du membre + + l'onglet n'a pas d'ordre de tri + + Où cette composition est-elle utilisée? + Cette composition est actuellement utilisée dans la composition des types de contenu suivants : - + + Fabrication des modèles + ceci peut prendre un peu de temps, ne vous inquiétez pas + Modèles générés + Les modèles n'ont pas pu être générés + La génération des modèles a échoué, voyez les exceptions dans les U log + + + + Ajouter un champ de rechange + Champ de rechange + Ajouter une valeur par défaut + Valeur par défaut Champ alternatif Texte alternatif Casse Encodage Choisir un champ Convertir les sauts de ligne + Oui, convertir les sauts de ligne Remplace les sauts de ligne avec des balises &lt;br&gt; Champs particuliers Oui, la date seulement + Format et encodage Formater comme une date + Formate la valeur comme une date, ou une date avec l'heure, en fonction de la culture active Encoder en HTML Remplacera les caractères spéciaux par leur équivalent HTML. Sera inséré après la valeur du champ Sera inséré avant la valeur du champ Minuscules + Modifier le résultat Aucun + Example de résultat Insérer après le champ Insérer avant le champ Récursif - Supprimer les balises de paragraphes - Supprimera toute balise &lt;P&gt; au début et à la fin du texte + Oui, rendre récursif + Séparateur Champs standards Majuscules Encode pour URL @@ -1166,10 +1727,12 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Tâches qui vous sont assignées - vous sont assignées. Pour voir un aperçu détaillé incluant les commentaires, cliquez sur "Détails" ou juste sur le nom de la page. + + vous sont assignées. Pour voir un aperçu détaillé incluant les commentaires, cliquez sur "Détails" ou juste sur le nom de la page. Vous pouvez aussi télécharger la page au format XML en cliquant sur le lien "Télécharger XML".
Pour clôturer une tâche de traduction, allez sur Détails, puis cliquez sur le bouton "Terminer la tâche". - ]]>
+ ]]> +
Terminer la tâche Détails Télécharger toutes les tâches de traductions au format XML @@ -1177,7 +1740,8 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Télécharger la DTD XML Champs Inclure les pages enfants - + + ]]> + [%0%] tâches de traductions pour %1% Aucun utilisateur traducteur trouvé. Veuillez créer un utilisateur traducteur avant d'envoyer du contenu pour traduction Tâches que vous avez créées - que vous avez créées. Pour voir un aperçu détaillé incluant les commentaires, + + que vous avez créées. Pour voir un aperçu détaillé incluant les commentaires, cliquez sur "Détails" ou juste sur le nom de la page. Vous pouvez aussi télécharger la page au format XML en cliquant sur le lien "Télécharger XML". Pour clôturer une tâche de traduction, allez sur Détails, puis cliquez sur le bouton "Terminer tâche". - ]]> + ]]> + La page '%0%' a été envoyée pour traduction Veuillez choisir la langue dans laquelle le contenu doit être traduit Envoyer la page '%0%' pour traduction @@ -1214,6 +1781,9 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Uploader le fichier de traduction XML + Contenu + Types de contenu + Media Navigateur de cache Corbeille Packages créés @@ -1234,6 +1804,8 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Types de relations Packages Packages + Vues Partielles + Vues Partielles pour les Fichiers Macro Fichiers Python Installer depuis le repository Installer Runway @@ -1242,8 +1814,8 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Scripts Feuilles de style Modèles - Permissions utilisateur - Types d'utilisateurs + + Analytique Utilisateurs Analytique @@ -1254,23 +1826,45 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Erreur lors de la recherche de mises à jour. Veuillez vérifier le stack trace pour obtenir plus d'informations sur l'erreur. + Accès + Sur base des groupes et des noeuds de départ, l'utilisateur a accès aux noeuds suivants + Donner accès Administrateur Champ catégorie + Utilisateur créé Changer le mot de passe + Changer la photo Nouveau mot de passe + n'a pas été bloqué + Le mot de passe n'a pas été modifié Confirmez votre nouveau mot de passe Vous pouvez changer votre mot de passe d'accès au Back Office Umbraco en remplissant le formulaire ci-dessous puis en cliquant sur le bouton "Changer le mot de passe" Canal de contenu + Créer un autre utilisateur + Créer de nouveaux utilisateurs pour leur donner accès à Umbraco. Lors de la création d'un nouvel utilisateur, un mot de passe est généré que vous pouvez partager avec ce dernier. Champ description Désactiver l'utilisateur Type de document Editeur Champ extrait + Tentatives de connexion échouées + Voir le profil de l'utilisateur + Ajouter des groupes pour donner les accès et permissions + Inviter un autre utilisateur + Inviter de nouveaux utilisateurs pour leur donner accès à Umbraco. Un email d'invitation sera envoyé à chaque utilisateur avec des informations concernant la connexion à Umbraco. Langue + Spécifiez la langue dans laquelle vous souhaitez voir les menus et dialogues + Date du dernier bloquage + Dernière connexion + Dernière modification du mot de passe Identifiant Noeud de départ dans la librarie de média + Limiter la librairie média à un noeud de départ spécifique + Noeuds de départ dans la librairie de média + Limiter la librairie média à des noeuds de départ spécifique Sections Désactiver l'accès Umbraco + ne s'est pas encore connecté Ancien mot de passe Mot de passe Réinitialiser le mot de passe @@ -1285,19 +1879,153 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Remplacer les permissions sur les noeuds enfants Vous êtes en train de modifiez les permissions pour les pages : Choisissez les pages dont les permissions doivent être modifiées + Supprimer la photo + Permissions par défaut + Permissions granulaires + Définir les permissions sur des noeuds spécifiques + Profil Rechercher tous les enfants + Ajouter les sections auxquelles les utilisateurs peuvent accéder + Sélectionner les groupes d'utilisateurs + Aucun noeud de départ sélectionné + Aucun noeud de départ sélectionné + Actif + Tous + Désactivé + Bloqué + Invité Noeud de départ du contenu + Limiter l'arborescence de contenu à un noeud de départ spécifique + Noeuds de départ du contenu + Limiter l'arborescence de contenu à des noeuds de départ spécifiques + Nom (A-Z) + Nom (Z-A) + Plus récent + Plus ancien + Dernière connexion + Dernière mise à jour de l'utilisateur + a été créé + Le nouvel utilisateur a été créé avec succès. Utilisez le mot de passe ci-dessous pour la connexion à Umbraco. + Gestion des utilisateurs Nom d'utilisateur - Permissions utilisateur - Type d'utilisateur - Types d'utilisateurs + Permissions de l'utilisateur + Permissions du groupe d'utilisateurs + Groupe d'utilisateurs + Groupes d'utilisateurs + a été invité + Une invitation a été envoyée au nouvel utilisateur avec les détails concernant la connexion à Umbraco. + Bien le bonjour et bienvenue dans Umbraco! Vous serez prêt.e dans moins d'1 minute, vous devez encore simplement configurer votre mot de passe et ajouter une photo pour votre avatar. + Chargez une photo afin que les autres utilisateurs puissent vous reconnaître facilement. Rédacteur Traducteur Modifier Votre profil Votre historique récent La session expire dans + Inviter un utilisateur + Créer un utilisateur + Envoyer l'invitation + Retour aux utilisateurs + Umbraco: Invitation + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Salut %0%, +

+

+ Vous avez été invité.e par %1% à accéder au Umbraco Back Office. +

+

+ Message de %1%: +
+ %2% +

+ + + + + + +
+ + + + + + +
+ + Cliquez sur ce lien pour accepter l'invitation + +
+
+

Si vous ne pouvez pas cliquer sur le lien, copiez cet URL dans votre navigateur :

+ + + + +
+ + %3% + +
+

+
+
+


+
+
+ + ]]> +
+ Inviter + Nouvel envoi de l'invitation en cours... + Supprimer l'Utilisateur + Etes-vous certain(e) de vouloir supprimer le compte de cet utilisateur? + Validation Valider comme email @@ -1305,6 +2033,14 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Valider comme Url ...ou introduisez une validation spécifique Champ obligatoire + Introduisez une expression régulière + Vous devez ajouter au moins + Vous ne pouvez avoir que + éléments + éléments sélectionnés + Date non valide + Pas un nombre + Email non valide @@ -1407,6 +2165,8 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à %0%.]]> %0%.]]> +

Les résultats de l'exécution du Umbraco Health Checks planifiée le %0% à %1% sont les suivants :

%2%]]>
+ Statut du Umbraco Health Check Désactiver URL tracker @@ -1425,4 +2185,10 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à URL tracker est maintenant activé. Erreur lors de l'activation de l'URL tracker, plus d'information disponible dans votre fichier log. + + Pas d'élément de dictionaire à choisir + + + caractères restant + diff --git a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx index cf71dc281e..46a27a4eba 100644 --- a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx +++ b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx @@ -39,14 +39,14 @@

Easy start with Umbraco.tv

We have created a bunch of 'how-to' videos, to get you easily started with Umbraco. Learn how to build projects in just a couple of minutes. Easiest CMS in the world.

- Umbraco.tv → + Umbraco.tv →

Be a part of the community

The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we're sure that you can get your answers from the community.

- our.Umbraco → + our.Umbraco →
diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config index c092aaae41..9a181a3ef8 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config @@ -40,7 +40,9 @@ - In Preview Mode - click to end]]> + In Preview Mode - click to end + ]]> - In Preview Mode - click to end + In Preview Mode - click to end ]]> diff --git a/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs b/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs index 43544c5af0..e865019790 100644 --- a/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs +++ b/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs @@ -1,5 +1,8 @@ using LightInject; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Mapping; +using Umbraco.Web.Trees; namespace Umbraco.Web.Composing.CompositionRoots { @@ -7,6 +10,7 @@ namespace Umbraco.Web.Composing.CompositionRoots { public void Compose(IServiceRegistry container) { + //register the profiles container.Register(); container.Register(); container.Register(); @@ -25,6 +29,16 @@ namespace Umbraco.Web.Composing.CompositionRoots container.Register(); container.Register(); container.Register(); + + //register any resolvers, etc.. that the profiles use + container.Register(); + container.Register>(); + container.Register>(); + container.Register>(); + container.Register>(); + container.Register(); + container.Register(); + container.Register(); } } } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 07d3fc45bb..92d392e890 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -64,7 +64,9 @@ namespace Umbraco.Web.Editors public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) { controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)))); + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)), + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) + )); } } @@ -284,6 +286,25 @@ namespace Umbraco.Web.Editors [OutgoingEditorModelEvent] [EnsureUserPermissionForContent("id")] public ContentItemDisplay GetById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + return null;//irrelevant since the above throws + } + var content = MapToDisplay(foundContent, culture); + return content; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Guid id, string culture = null) { var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); if (foundContent == null) @@ -296,6 +317,24 @@ namespace Umbraco.Web.Editors return content; } + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Udi id, string culture = null) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetById(guidUdi.Guid, culture); + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + /// /// Gets an empty content item for the /// diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index a756c527d3..77ff974aaa 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -21,11 +21,12 @@ namespace Umbraco.Web.Editors /// An API controller used for dealing with member types /// [PluginController("UmbracoApi")] - [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] + [UmbracoTreeAuthorize(new string[] { Constants.Trees.MemberTypes, Constants.Trees.Members})] public class MemberTypeController : ContentTypeControllerBase { private readonly MembershipProvider _provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public MemberTypeDisplay GetById(int id) { var ct = Services.MemberTypeService.Get(id); @@ -45,6 +46,7 @@ namespace Umbraco.Web.Editors /// [HttpDelete] [HttpPost] + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public HttpResponseMessage DeleteById(int id) { var foundType = Services.MemberTypeService.Get(id); @@ -71,6 +73,8 @@ namespace Umbraco.Web.Editors /// be looked up via the db, they need to be passed in. /// /// + + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public HttpResponseMessage GetAvailableCompositeMemberTypes(int contentTypeId, [FromUri]string[] filterContentTypes, [FromUri]string[] filterPropertyTypes) @@ -84,6 +88,7 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(result); } + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public MemberTypeDisplay GetEmpty() { var ct = new MemberType(-1); @@ -107,6 +112,7 @@ namespace Umbraco.Web.Editors return Enumerable.Empty(); } + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public MemberTypeDisplay PostSave(MemberTypeSave contentTypeSave) { //get the persisted member type diff --git a/src/Umbraco.Web/Editors/TemplateController.cs b/src/Umbraco.Web/Editors/TemplateController.cs index c4e87d0648..fae7bfdfbf 100644 --- a/src/Umbraco.Web/Editors/TemplateController.cs +++ b/src/Umbraco.Web/Editors/TemplateController.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; +using AutoMapper; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; -using AutoMapper; using Umbraco.Core.IO; using Umbraco.Core.Models; -using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs index 622220ab78..4b49a21606 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs @@ -16,25 +16,24 @@ namespace Umbraco.Web.Models.Mapping internal class ContentMapperProfile : Profile { public ContentMapperProfile( + ContentUrlResolver contentUrlResolver, + ContentTreeNodeUrlResolver contentTreeNodeUrlResolver, + TabsAndPropertiesResolver tabsAndPropertiesResolver, IUserService userService, ILocalizedTextService textService, IContentService contentService, IContentTypeService contentTypeService, IDataTypeService dataTypeService, ILocalizationService localizationService, - PropertyEditorCollection propertyEditors, ILogger logger) { // create, capture, cache var contentOwnerResolver = new OwnerResolver(userService); var creatorResolver = new CreatorResolver(userService); var actionButtonsResolver = new ActionButtonsResolver(userService, contentService); - var tabsAndPropertiesResolver = new TabsAndPropertiesResolver(textService); var childOfListViewResolver = new ContentChildOfListViewResolver(contentService, contentTypeService); var contentTypeBasicResolver = new ContentTypeBasicResolver(); - var contentTreeNodeUrlResolver = new ContentTreeNodeUrlResolver(); var defaultTemplateResolver = new DefaultTemplateResolver(); - var contentUrlResolver = new ContentUrlResolver(textService, contentService, logger); var variantResolver = new ContentVariantResolver(localizationService, textService); var contentAppResolver = new ContentAppResolver(dataTypeService, propertyEditors); diff --git a/src/Umbraco.Web/Models/Mapping/ContentTreeNodeUrlResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentTreeNodeUrlResolver.cs index 48400f51f6..91420fbe21 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTreeNodeUrlResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTreeNodeUrlResolver.cs @@ -12,9 +12,16 @@ namespace Umbraco.Web.Models.Mapping where TSource : IContentBase where TController : ContentTreeControllerBase { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public ContentTreeNodeUrlResolver(IUmbracoContextAccessor umbracoContextAccessor) + { + _umbracoContextAccessor = umbracoContextAccessor ?? throw new System.ArgumentNullException(nameof(umbracoContextAccessor)); + } + public string Resolve(TSource source, object destination, string destMember, ResolutionContext context) { - var umbracoContext = context.GetUmbracoContext(throwIfMissing: false); + var umbracoContext = _umbracoContextAccessor.UmbracoContext; if (umbracoContext == null) return null; var urlHelper = new UrlHelper(umbracoContext.HttpContext.Request.RequestContext); diff --git a/src/Umbraco.Web/Models/Mapping/ContentUrlResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentUrlResolver.cs index a278a4a8c5..dd235bda75 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentUrlResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentUrlResolver.cs @@ -10,24 +10,36 @@ namespace Umbraco.Web.Models.Mapping { internal class ContentUrlResolver : IValueResolver { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly PublishedRouter _publishedRouter; + private readonly ILocalizationService _localizationService; private readonly ILocalizedTextService _textService; private readonly IContentService _contentService; private readonly ILogger _logger; - public ContentUrlResolver(ILocalizedTextService textService, IContentService contentService, ILogger logger) + public ContentUrlResolver( + IUmbracoContextAccessor umbracoContextAccessor, + PublishedRouter publishedRouter, + ILocalizationService localizationService, + ILocalizedTextService textService, + IContentService contentService, + ILogger logger) { - _textService = textService; - _contentService = contentService; - _logger = logger; + _umbracoContextAccessor = umbracoContextAccessor ?? throw new System.ArgumentNullException(nameof(umbracoContextAccessor)); + _publishedRouter = publishedRouter ?? throw new System.ArgumentNullException(nameof(publishedRouter)); + _localizationService = localizationService ?? throw new System.ArgumentNullException(nameof(localizationService)); + _textService = textService ?? throw new System.ArgumentNullException(nameof(textService)); + _contentService = contentService ?? throw new System.ArgumentNullException(nameof(contentService)); + _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); } public UrlInfo[] Resolve(IContent source, ContentItemDisplay destination, UrlInfo[] destMember, ResolutionContext context) { - var umbracoContext = context.GetUmbracoContext(throwIfMissing: false); + var umbracoContext = _umbracoContextAccessor.UmbracoContext; var urls = umbracoContext == null ? new[] { UrlInfo.Message("Cannot generate urls without a current Umbraco Context") } - : source.GetContentUrls(umbracoContext.UrlProvider, _textService, _contentService, _logger).ToArray(); + : source.GetContentUrls(_publishedRouter, umbracoContext, _localizationService, _textService, _contentService, _logger).ToArray(); return urls; } diff --git a/src/Umbraco.Web/Models/Mapping/ContextMapper.cs b/src/Umbraco.Web/Models/Mapping/ContextMapper.cs index 5aa8af7668..6897a8ae62 100644 --- a/src/Umbraco.Web/Models/Mapping/ContextMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContextMapper.cs @@ -10,42 +10,9 @@ namespace Umbraco.Web.Models.Mapping /// internal static class ContextMapper { - public const string UmbracoContextKey = "ContextMapper.UmbracoContext"; + //public const string UmbracoContextKey = "ContextMapper.UmbracoContext"; public const string CultureKey = "ContextMapper.Culture"; - - public static TDestination Map(TSource obj, UmbracoContext umbracoContext) - => Mapper.Map(obj, opt => opt.Items[UmbracoContextKey] = umbracoContext); - - public static TDestination Map(TSource obj, UmbracoContext umbracoContext, IDictionary contextVals) - => Mapper.Map(obj, opt => - { - //set the umb ctx - opt.Items[UmbracoContextKey] = umbracoContext; - //set other supplied context vals - if (contextVals != null) - { - foreach (var contextVal in contextVals) - { - opt.Items[contextVal.Key] = contextVal.Value; - } - } - }); - - public static TDestination Map(TSource obj, UmbracoContext umbracoContext, object contextVals) - => Mapper.Map(obj, opt => - { - //set the umb ctx - opt.Items[UmbracoContextKey] = umbracoContext; - //set other supplied context vals - if (contextVals != null) - { - foreach (var contextVal in contextVals.ToDictionary()) - { - opt.Items[contextVal.Key] = contextVal.Value; - } - } - }); - + public static TDestination Map(TSource obj, IDictionary contextVals) => Mapper.Map(obj, opt => { @@ -86,27 +53,6 @@ namespace Umbraco.Web.Models.Mapping return null; } - - /// - /// Returns the in the mapping context if one is found - /// - /// - /// - /// - public static UmbracoContext GetUmbracoContext(this ResolutionContext resolutionContext, bool throwIfMissing = true) - { - if (resolutionContext.Options.Items.TryGetValue(UmbracoContextKey, out var obj) && obj is UmbracoContext umbracoContext) - return umbracoContext; - - // better fail fast - if (throwIfMissing) - throw new InvalidOperationException("AutoMapper ResolutionContext does not contain an UmbracoContext."); - - // fixme - not a good idea at all - // because this falls back to magic singletons - // so really we should remove this line, but then some tests+app breaks ;( - return Umbraco.Web.Composing.Current.UmbracoContext; - } } } diff --git a/src/Umbraco.Web/Models/Mapping/MediaMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/MediaMapperProfile.cs index 1b0d5cb35e..fa0f576ee9 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaMapperProfile.cs @@ -16,19 +16,18 @@ namespace Umbraco.Web.Models.Mapping internal class MediaMapperProfile : Profile { public MediaMapperProfile( + TabsAndPropertiesResolver tabsAndPropertiesResolver, + ContentTreeNodeUrlResolver contentTreeNodeUrlResolver, IUserService userService, ILocalizedTextService textService, IDataTypeService dataTypeService, IMediaService mediaService, IMediaTypeService mediaTypeService, - PropertyEditorCollection propertyEditors, ILogger logger) { // create, capture, cache var mediaOwnerResolver = new OwnerResolver(userService); - var tabsAndPropertiesResolver = new TabsAndPropertiesResolver(textService); var childOfListViewResolver = new MediaChildOfListViewResolver(mediaService, mediaTypeService); - var contentTreeNodeUrlResolver = new ContentTreeNodeUrlResolver(); var mediaTypeBasicResolver = new ContentTypeBasicResolver(); var mediaAppResolver = new MediaAppResolver(dataTypeService, propertyEditors); diff --git a/src/Umbraco.Web/Models/Mapping/MemberBasicPropertiesResolver.cs b/src/Umbraco.Web/Models/Mapping/MemberBasicPropertiesResolver.cs index 09d0657530..fbd14876ea 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberBasicPropertiesResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberBasicPropertiesResolver.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using AutoMapper; using Umbraco.Core.Models; @@ -11,9 +12,17 @@ namespace Umbraco.Web.Models.Mapping /// internal class MemberBasicPropertiesResolver : IValueResolver> { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public MemberBasicPropertiesResolver(IUmbracoContextAccessor umbracoContextAccessor) + { + _umbracoContextAccessor = umbracoContextAccessor ?? throw new System.ArgumentNullException(nameof(umbracoContextAccessor)); + } + public IEnumerable Resolve(IMember source, MemberBasic destination, IEnumerable destMember, ResolutionContext context) { - var umbracoContext = context.GetUmbracoContext(); + var umbracoContext = _umbracoContextAccessor.UmbracoContext; + if (umbracoContext == null) throw new InvalidOperationException("Cannot resolve value without an UmbracoContext available"); var result = Mapper.Map, IEnumerable>( // Sort properties so items from different compositions appear in correct order (see U4-9298). Map sorted properties. @@ -39,4 +48,4 @@ namespace Umbraco.Web.Models.Mapping return result; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Models/Mapping/MemberMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/MemberMapperProfile.cs index d0ab4099a9..ae4fc19d54 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberMapperProfile.cs @@ -14,16 +14,19 @@ namespace Umbraco.Web.Models.Mapping /// internal class MemberMapperProfile : Profile { - public MemberMapperProfile(IUserService userService, ILocalizedTextService textService, IMemberTypeService memberTypeService, IMemberService memberService) + public MemberMapperProfile( + MemberTabsAndPropertiesResolver tabsAndPropertiesResolver, + MemberTreeNodeUrlResolver memberTreeNodeUrlResolver, + MemberBasicPropertiesResolver memberBasicPropertiesResolver, + IUserService userService, + IMemberTypeService memberTypeService, + IMemberService memberService) { // create, capture, cache var memberOwnerResolver = new OwnerResolver(userService); - var tabsAndPropertiesResolver = new MemberTabsAndPropertiesResolver(textService, memberService, userService); var memberProfiderFieldMappingResolver = new MemberProviderFieldResolver(); var membershipScenarioMappingResolver = new MembershipScenarioResolver(memberTypeService); var memberDtoPropertiesResolver = new MemberDtoPropertiesResolver(); - var memberTreeNodeUrlResolver = new MemberTreeNodeUrlResolver(); - var memberBasicPropertiesResolver = new MemberBasicPropertiesResolver(); //FROM MembershipUser TO MediaItemDisplay - used when using a non-umbraco membership provider CreateMap().ConvertUsing(); diff --git a/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesResolver.cs b/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesResolver.cs index 66fb3619cf..9f6747ea95 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesResolver.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Web.Security; using AutoMapper; @@ -22,24 +23,27 @@ namespace Umbraco.Web.Models.Mapping /// internal class MemberTabsAndPropertiesResolver : TabsAndPropertiesResolver { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly ILocalizedTextService _localizedTextService; private readonly IMemberService _memberService; private readonly IUserService _userService; - public MemberTabsAndPropertiesResolver(ILocalizedTextService localizedTextService, IMemberService memberService, IUserService userService) + public MemberTabsAndPropertiesResolver(IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService localizedTextService, IMemberService memberService, IUserService userService) : base(localizedTextService) { - _localizedTextService = localizedTextService; - _memberService = memberService; - _userService = userService; + _umbracoContextAccessor = umbracoContextAccessor ?? throw new System.ArgumentNullException(nameof(umbracoContextAccessor)); + _localizedTextService = localizedTextService ?? throw new System.ArgumentNullException(nameof(localizedTextService)); + _memberService = memberService ?? throw new System.ArgumentNullException(nameof(memberService)); + _userService = userService ?? throw new System.ArgumentNullException(nameof(userService)); } - public MemberTabsAndPropertiesResolver(ILocalizedTextService localizedTextService, IEnumerable ignoreProperties, IMemberService memberService, IUserService userService) + public MemberTabsAndPropertiesResolver(IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService localizedTextService, IEnumerable ignoreProperties, IMemberService memberService, IUserService userService) : base(localizedTextService, ignoreProperties) { - _localizedTextService = localizedTextService; - _memberService = memberService; - _userService = userService; + _umbracoContextAccessor = umbracoContextAccessor ?? throw new System.ArgumentNullException(nameof(umbracoContextAccessor)); + _localizedTextService = localizedTextService ?? throw new System.ArgumentNullException(nameof(localizedTextService)); + _memberService = memberService ?? throw new System.ArgumentNullException(nameof(memberService)); + _userService = userService ?? throw new System.ArgumentNullException(nameof(userService)); } /// @@ -80,7 +84,7 @@ namespace Umbraco.Web.Models.Mapping } } - var umbracoContext = context.GetUmbracoContext(); + var umbracoContext = _umbracoContextAccessor.UmbracoContext; if (umbracoContext != null && umbracoContext.Security.CurrentUser != null && umbracoContext.Security.CurrentUser.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings))) @@ -166,24 +170,25 @@ namespace Umbraco.Web.Models.Mapping /// /// Overridden to assign the IsSensitive property values /// - /// /// /// /// /// - protected override List MapProperties(UmbracoContext umbracoContext, IContentBase content, List properties, ResolutionContext context) + protected override List MapProperties(IContentBase content, List properties, ResolutionContext context) { - var result = base.MapProperties(umbracoContext, content, properties, context); + var result = base.MapProperties(content, properties, context); var member = (IMember)content; var memberType = member.ContentType; + var umbracoContext = _umbracoContextAccessor.UmbracoContext; + //now update the IsSensitive value foreach (var prop in result) { //check if this property is flagged as sensitive var isSensitiveProperty = memberType.IsSensitiveProperty(prop.Alias); //check permissions for viewing sensitive data - if (isSensitiveProperty && umbracoContext.Security.CurrentUser.HasAccessToSensitiveData() == false) + if (isSensitiveProperty && (umbracoContext == null || umbracoContext.Security.CurrentUser.HasAccessToSensitiveData() == false)) { //mark this property as sensitive prop.IsSensitive = true; diff --git a/src/Umbraco.Web/Models/Mapping/MemberTreeNodeUrlResolver.cs b/src/Umbraco.Web/Models/Mapping/MemberTreeNodeUrlResolver.cs index 864fd18ab2..c4655294d7 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberTreeNodeUrlResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberTreeNodeUrlResolver.cs @@ -11,9 +11,16 @@ namespace Umbraco.Web.Models.Mapping /// internal class MemberTreeNodeUrlResolver : IValueResolver { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public MemberTreeNodeUrlResolver(IUmbracoContextAccessor umbracoContextAccessor) + { + _umbracoContextAccessor = umbracoContextAccessor ?? throw new System.ArgumentNullException(nameof(umbracoContextAccessor)); + } + public string Resolve(IMember source, MemberDisplay destination, string destMember, ResolutionContext context) { - var umbracoContext = context.GetUmbracoContext(throwIfMissing: false); + var umbracoContext = _umbracoContextAccessor.UmbracoContext; if (umbracoContext == null) return null; var urlHelper = new UrlHelper(umbracoContext.HttpContext.Request.RequestContext); diff --git a/src/Umbraco.Web/Models/Mapping/MembershipUserTypeConverter.cs b/src/Umbraco.Web/Models/Mapping/MembershipUserTypeConverter.cs index dbbf6f69df..38482cca49 100644 --- a/src/Umbraco.Web/Models/Mapping/MembershipUserTypeConverter.cs +++ b/src/Umbraco.Web/Models/Mapping/MembershipUserTypeConverter.cs @@ -13,9 +13,9 @@ namespace Umbraco.Web.Models.Mapping public MemberDisplay Convert(MembershipUser source, MemberDisplay destination, ResolutionContext context) { //first convert to IMember - var member = Mapper.Map(source); + var member = Mapper.Map(source); //then convert to MemberDisplay - return ContextMapper.Map(member, context.GetUmbracoContext()); + return Mapper.Map(member); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs index 417466231c..fa4af3c39b 100644 --- a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs @@ -53,7 +53,6 @@ namespace Umbraco.Web.Models.Mapping /// /// Maps properties on to the generic properties tab /// - /// /// /// /// @@ -61,14 +60,14 @@ namespace Umbraco.Web.Models.Mapping /// The generic properties tab is responsible for /// setting up the properties such as Created date, updated date, template selected, etc... /// - protected virtual void MapGenericProperties(UmbracoContext umbracoContext, IContentBase content, List> tabs, ResolutionContext context) + protected virtual void MapGenericProperties(IContentBase content, List> tabs, ResolutionContext context) { // add the generic properties tab, for properties that don't belong to a tab // get the properties, map and translate them, then add the tab var noGroupProperties = content.GetNonGroupedProperties() .Where(x => IgnoreProperties.Contains(x.Alias) == false) // skip ignored .ToList(); - var genericproperties = MapProperties(umbracoContext, content, noGroupProperties, context); + var genericproperties = MapProperties(content, noGroupProperties, context); tabs.Add(new Tab { @@ -114,12 +113,11 @@ namespace Umbraco.Web.Models.Mapping /// /// Maps a list of to a list of /// - /// /// /// /// /// - protected virtual List MapProperties(UmbracoContext umbracoContext, IContentBase content, List properties, ResolutionContext context) + protected virtual List MapProperties(IContentBase content, List properties, ResolutionContext context) { //we need to map this way to pass the context through, I don't like it but we'll see what AutoMapper says: https://github.com/AutoMapper/AutoMapper/issues/2588 var result = context.Mapper.Map, IEnumerable>( @@ -140,15 +138,16 @@ namespace Umbraco.Web.Models.Mapping { public TabsAndPropertiesResolver(ILocalizedTextService localizedTextService) : base(localizedTextService) - { } + { + } public TabsAndPropertiesResolver(ILocalizedTextService localizedTextService, IEnumerable ignoreProperties) : base(localizedTextService, ignoreProperties) - { } + { + } public virtual IEnumerable> Resolve(TSource source, TDestination destination, IEnumerable> destMember, ResolutionContext context) { - var umbracoContext = context.GetUmbracoContext(throwIfMissing: false); // fixme var tabs = new List>(); // add the tabs, for properties that belong to a tab @@ -173,7 +172,7 @@ namespace Umbraco.Web.Models.Mapping continue; //map the properties - var mappedProperties = MapProperties(umbracoContext, source, properties, context); + var mappedProperties = MapProperties(source, properties, context); // add the tab // we need to pick an identifier... there is no "right" way... @@ -191,7 +190,7 @@ namespace Umbraco.Web.Models.Mapping }); } - MapGenericProperties(umbracoContext, source, tabs, context); + MapGenericProperties(source, tabs, context); // activate the first tab, if any if (tabs.Count > 0) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 5e1708f118..d3ff363156 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -385,7 +385,7 @@ namespace Umbraco.Web public static bool IsDescendant(this IPublishedContent content, IPublishedContent other) { - return content.Ancestors().Any(x => x.Id == other.Id); + return other.Level < content.Level && content.Path.InvariantStartsWith(other.Path); } public static HtmlString IsDescendant(this IPublishedContent content, IPublishedContent other, string valueIfTrue) @@ -400,7 +400,7 @@ namespace Umbraco.Web public static bool IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other) { - return content.AncestorsOrSelf().Any(x => x.Id == other.Id); + return content.Path.InvariantStartsWith(other.Path); } public static HtmlString IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue) @@ -415,8 +415,8 @@ namespace Umbraco.Web public static bool IsAncestor(this IPublishedContent content, IPublishedContent other) { - // avoid using Descendants(), that's expensive - return other.Ancestors().Any(x => x.Id == content.Id); + // avoid using Descendants(), or Ancestors(), they're expensive + return content.Level < other.Level && other.Path.InvariantStartsWith(content.Path); } public static HtmlString IsAncestor(this IPublishedContent content, IPublishedContent other, string valueIfTrue) @@ -431,8 +431,8 @@ namespace Umbraco.Web public static bool IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other) { - // avoid using DescendantsOrSelf(), that's expensive - return other.AncestorsOrSelf().Any(x => x.Id == content.Id); + // avoid using DescendantsOrSelf() or AncestorsOrSelf(), they're expensive + return other.Path.InvariantStartsWith(content.Path); } public static HtmlString IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue) diff --git a/src/Umbraco.Web/Routing/PublishedRequest.cs b/src/Umbraco.Web/Routing/PublishedRequest.cs index 492837104d..17a9cc89e1 100644 --- a/src/Umbraco.Web/Routing/PublishedRequest.cs +++ b/src/Umbraco.Web/Routing/PublishedRequest.cs @@ -78,6 +78,11 @@ namespace Umbraco.Web.Routing _publishedRouter.PrepareRequest(this); } + /// + /// Gets or sets a value indicating whether the Umbraco Backoffice should ignore a collision for this request. + /// + public bool IgnorePublishedContentCollisions { get; set; } + #region Events /// diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index 9f1d42d6b1..ce09bdc645 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -18,10 +18,18 @@ namespace Umbraco.Web.Routing /// Use when displaying Urls. If errors occur when generating the Urls, they will show in the list. /// Contains all the Urls that we can figure out (based upon domains, etc). /// - public static IEnumerable GetContentUrls(this IContent content, UrlProvider urlProvider, ILocalizedTextService textService, IContentService contentService, ILogger logger) + public static IEnumerable GetContentUrls(this IContent content, + PublishedRouter publishedRouter, + UmbracoContext umbracoContext, + ILocalizationService localizationService, + ILocalizedTextService textService, + IContentService contentService, + ILogger logger) { if (content == null) throw new ArgumentNullException(nameof(content)); - if (urlProvider == null) throw new ArgumentNullException(nameof(urlProvider)); + if (publishedRouter == null) throw new ArgumentNullException(nameof(publishedRouter)); + if (umbracoContext == null) throw new ArgumentNullException(nameof(umbracoContext)); + if (localizationService == null) throw new ArgumentNullException(nameof(localizationService)); if (textService == null) throw new ArgumentNullException(nameof(textService)); if (contentService == null) throw new ArgumentNullException(nameof(contentService)); if (logger == null) throw new ArgumentNullException(nameof(logger)); @@ -33,12 +41,7 @@ namespace Umbraco.Web.Routing urls.Add(UrlInfo.Message(textService.Localize("content/itemNotPublished"))); return urls; } - - // fixme inject - // fixme PublishedRouter is stateless and should be a singleton! - var localizationService = Core.Composing.Current.Services.LocalizationService; - var publishedRouter = Core.Composing.Current.Container.GetInstance(); - + // build a list of urls, for the back-office // which will contain // - the 'main' urls, which is what .Url would return, for each culture @@ -61,7 +64,7 @@ namespace Umbraco.Web.Routing string url; try { - url = urlProvider.GetUrl(content.Id, culture); + url = umbracoContext.UrlProvider.GetUrl(content.Id, culture); } catch (Exception e) { @@ -83,7 +86,7 @@ namespace Umbraco.Web.Routing // got a url, deal with collisions, add url default: - if (!DetectCollision(content, url, urls, culture, publishedRouter, textService)) // detect collisions, etc + if (!DetectCollision(content, url, urls, culture, umbracoContext, publishedRouter, textService)) // detect collisions, etc urls.Add(UrlInfo.Url(url, culture)); break; } @@ -137,13 +140,13 @@ namespace Umbraco.Web.Routing urls.Add(UrlInfo.Message(textService.Localize("content/parentCultureNotPublished", new[] { parent.Name }), culture)); } - private static bool DetectCollision(IContent content, string url, List urls, string culture, PublishedRouter publishedRouter, ILocalizedTextService textService) + private static bool DetectCollision(IContent content, string url, List urls, string culture, UmbracoContext umbracoContext, PublishedRouter publishedRouter, ILocalizedTextService textService) { // test for collisions on the 'main' url var uri = new Uri(url.TrimEnd('/'), UriKind.RelativeOrAbsolute); - if (uri.IsAbsoluteUri == false) uri = uri.MakeAbsolute(UmbracoContext.Current.CleanedUmbracoUrl); + if (uri.IsAbsoluteUri == false) uri = uri.MakeAbsolute(umbracoContext.CleanedUmbracoUrl); uri = UriUtility.UriToUmbraco(uri); - var pcr = publishedRouter.CreateRequest(UmbracoContext.Current, uri); + var pcr = publishedRouter.CreateRequest(umbracoContext, uri); publishedRouter.TryRouteRequest(pcr); if (pcr.HasPublishedContent == false) @@ -152,6 +155,9 @@ namespace Umbraco.Web.Routing return true; } + if (pcr.IgnorePublishedContentCollisions) + return false; + if (pcr.PublishedContent.Id != content.Id) { var o = pcr.PublishedContent; diff --git a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs index a4e5db0767..03ba763527 100644 --- a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs +++ b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs @@ -192,7 +192,7 @@ namespace Umbraco.Web.Runtime composition.Container.RegisterAuto(typeof(UmbracoViewPage<>)); // register published router - composition.Container.Register(); + composition.Container.RegisterSingleton(); composition.Container.Register(_ => UmbracoConfig.For.UmbracoSettings().WebRouting); // register preview SignalR hub diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index b2bf3f358e..bf51d8b6af 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -587,7 +587,6 @@ namespace Umbraco.Web return ContentQuery.ContentAtRoot(); } - /// Had to change to internal for testing. internal static bool ConvertIdObjectToInt(object id, out int intId) { switch (id) @@ -605,7 +604,6 @@ namespace Umbraco.Web } } - /// Had to change to internal for testing. internal static bool ConvertIdObjectToGuid(object id, out Guid guidId) { switch (id) diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs index bf922da15b..4d9a6db129 100644 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs @@ -6,6 +6,8 @@ using Umbraco.Core.Exceptions; using Umbraco.Web.Composing; using Umbraco.Web.Editors; using Umbraco.Web._Legacy.Actions; +using Umbraco.Core; +using Umbraco.Core.Models; namespace Umbraco.Web.WebApi.Filters { @@ -68,7 +70,25 @@ namespace Umbraco.Web.WebApi.Filters if (parts.Length == 1) { - nodeId = (int)actionContext.ActionArguments[parts[0]]; + var argument = actionContext.ActionArguments[parts[0]].ToString(); + // if the argument is an int, it will parse and can be assigned to nodeId + // if might be a udi, so check that next + // otherwise treat it as a guid - unlikely we ever get here + if (int.TryParse(argument, out int parsedId)) + { + nodeId = parsedId; + } + else if (Udi.TryParse(argument, true, out Udi udi)) + { + //fixme: inject? we can't because this is an attribute but we could provide ctors and empty ctors that pass in the required services + nodeId = Current.Services.EntityService.GetId(udi).Result; + } + else + { + Guid.TryParse(argument, out Guid key); + //fixme: inject? we can't because this is an attribute but we could provide ctors and empty ctors that pass in the required services + nodeId = Current.Services.EntityService.GetId(key, UmbracoObjectTypes.Document).Result; + } } else { @@ -89,7 +109,8 @@ namespace Umbraco.Web.WebApi.Filters if (ContentController.CheckPermissions( actionContext.Request.Properties, - UmbracoContext.Current.Security.CurrentUser, + //fixme: inject? we can't because this is an attribute but we could provide ctors and empty ctors that pass in the required services + Current.UmbracoContext.Security.CurrentUser, Current.Services.UserService, Current.Services.ContentService, Current.Services.EntityService,