From 98d8971ce9170571a90a334e5180d8dfac28af1f Mon Sep 17 00:00:00 2001 From: Chad Date: Wed, 21 Apr 2021 11:05:01 +1200 Subject: [PATCH] Merge in v8/contrib to V8/feature/nucache perf sync (#10151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bump version to 8.6.8 * Initial rework of Lock dictionaries * [Issue 5277-146] accessibility - Close 'X' icon next to language drop… (#9264) * [Issue 5277-146] accessibility - Close 'X' icon next to language drop down is identified as "link" - screen reader * add new loacalization key * Fix issue with SqlMainDomLock that cannot use implicit lock timeouts … (#9973) * Fix issue with SqlMainDomLock that cannot use implicit lock timeouts … (#9973) (cherry picked from commit da5351dfcf23daad69fcd73eb74811456ffc34c0) * Adjust unit tests and apply fixes to scope * Add more unit tests, showing current issue * Counting Umbraco.ModelsBuilder and ModelsBuilder.Umbraco namespaces as external providers * Fix dead lock with TypeLoader * Fix errors shown in unit tests * Throw error if all scopes hasn't been disposed * Clean * Fixes and Updates for DB Scope and Ambient Context leaks (#9953) * Adds some scope tests (ported back from netcore) and provides a much better error message, ensure execution context is not flowed to child tasks that shouldn't leak any current ambient context * updates comment * Ensure SqlMainDomLock suppresses execution context too * Since we're awaiting a task in a library method, ConfigureAwait(false) * missing null check Co-authored-by: Elitsa Marinovska * Adds additional error checking and reporting to MainDom/SqlMainDomLock (#9954) Co-authored-by: Elitsa Marinovska * Add copy logic to Media Picker (#9957) * Add copy logic to Media Picker * Add action for copy all * Fix for selectable media item * Wrap calls to map in scopes * Autocomplete scopes * Remove unnecessary aria-hidden attribute from * Remove scope from method that calls another method that has a scope * Fixes #9993 - Cannot save empty image in Grid * Clean * Revert "The Value() method for IPublishedContent was not working with the defaultValue parameter" (#9989) * Use a hashset to keep track of acquired locks This simplifies disposing/checking for locks greatly. * Add images in grid - fixes 9982 (#9987) Co-authored-by: Sebastiaan Janssen * Only create the dicts and hashset when a lock is requested * Clean * Adds a config for configuring the access rules on the content dashboard - by default it granted for all user groups * Adds additional params indicating whether user is admin * Add images in grid - fixes 9982 (#9987) Co-authored-by: Sebastiaan Janssen (cherry picked from commit e2019777fbfc1f9221d040cb9f0b82c57f8552b9) * Bump version to 8.12.2 * #9964 Removed unneeded check for HttpContext * Fix for #9950 - HttpsCheck will now retry using the login background image if inital request returns 301/302. Excessvie Headers check will now check the root url instead of the backoffice * Merge pull request #9994 from umbraco/v8/bugfix/9993 Fixes #9993 - Cannot save empty image in Grid (cherry picked from commit 0ecc933921f2dea9a2a16d6f395b44a039663ec6) * Apply suggestions from review * Fixes #9983 - Getting kicked, if document type has a Umbraco.UserPicker property (#10002) * Fixes #9983 Temporary fix for this issue. using the entityservice like before. * Needed to remove the call to usersResource here as well for displaying the picked items * Don't need usersResource for now * Fixes #9983 - Getting kicked, if document type has a Umbraco.UserPicker property (#10002) * Fixes #9983 Temporary fix for this issue. using the entityservice like before. * Needed to remove the call to usersResource here as well for displaying the picked items * Don't need usersResource for now (cherry picked from commit 45de0a101eaa2b8f16e21a765f32928c7cb968be) * 8539: Allow alias in image cropper (#9266) Co-authored-by: Owain Williams * Wrap dumping dictionaries in a method. * Create method for generating log message And remove forgotten comments. * Fix swedish translation for somethingElse. * Copy member type (#10020) * Add copy dialog for member type * Implement copy action for member type * Create specific localization for content type, media type and member type * Handle "foldersonly" querystring * Add button type attribute * Add a few missing changes of anchor to button element * Null check on scope and options to ensure backward compatibility * Improve performance, readability and handling of FollowInternalRedirects (#9889) * Improve performance, readability and handling of FollowInternalRedirects * Logger didn't like string param Passing string param to _logger.Debug wasn't happy. Changed to pass existing internalRedirectAsInt variable. Co-authored-by: Nathan Woulfe * Update casing of listview layout name * 9097 add contextual password helper (#9256) * update back-office forms * Display tip on reset password page as well * add directive for password tip * integrate directove in login screen * forgot the ng-keyup :-) * adapt tooltip directive to potential different Members and Users password settings * remove watcher Co-authored-by: Nathan Woulfe * Unbind listener Listening for splitViewRequest was only unbound if the split view editor was opened. Not cleaning up the listener caused a memory leak when changing between nodes as the spit view editor was detached but not garbage-collected * Replace icon in date picker with umb-icon component (#10040) * Replace icon in date picker with component * Adjust height of clear button * Update cypress and fix tests * Listview config icons (#10036) * Update icons to use component * Simplify markup and use disabled button * Use move cursor style on sortable handle * Add class for action column * Update setting auto focus * Increase font size of umb-panel-header-icon * Anchor noopener (#10009) * Set rel="noopener" for anchors with target="_blank" * Reverted unwanted changes to Default.cshtml * Align 'Add language' test to netcore * Add new cypress tests * Add indentation * Getting rid of the config file and implementing an appSetting instead * Implementation for IContentDashboardSettings * Cleanup * bool.Try * Taking AllowContentDashboardAccessToAllUsers prop from GlobalSettings to ContentDashboardSettings and saving AccessRulesFromConfig into a backing field * Handling multiple values per field in Examine Management * Add Root and Breadcrumbs extension methods for IPublishedContent (#9033) * Fix usage of obsolete CreatorName and WriterName properties * Add generic Root extension method * Add Breadcrumbs extension methods * Orders member type grouping of members alphabetically, matching the listing of member types. * Revert updating deprecated WriterName/CreatorName refs Changing the properties to use the extensions is a good thing (given the props are deprecated), but causes issues deep in tests. I'm reverting that change to fix the tests, and all refs to the deprecated properties should be updated in one sweep, to deal with any other test issues that might crop up. * Handle Invalid format for Upgrade check * Fixes tabbing-mode remains active after closing modal #9790 (#10074) * Allow to pass in boolean to preventEnterSubmit directive (#8639) * Pass in value to preventEnterSubmit directive * Set enabled similar to preventDefault and preventEnterSubmit directives * Update prevent enter submit value * Init value from controller * Use a different default input id prefix for umb-search-filter * Fix typo * Check for truthly value * Revert "Set enabled similar to preventDefault and preventEnterSubmit directives" This reverts commit 536ce855c4545ead82cea77b4013bf9010a8687b. * None pointer events when clicking icon * Use color variable * Fixes tabbing-mode remains active after closing modal #9790 (#10074) (cherry picked from commit c881fa9e7d08c11954e18489827f70cdafceb947) * Null check on scope and options to ensure backward compatibility (cherry picked from commit fe8cd239d2f4c528c1a8a3cf4c50e90bb43cacfc) * Fix validation of step size in integer/numeric field * 9962: Use $allowedEditors instead of allowed (#10086) * 9962: Use $allowedEditors instead of allowed * 9962: Remove redundant statement * fixes #10021 adds ng-form and val-form-manager to the documentation * Improved accessibility of link picker (#10099) * Added support for screeen reader alerts on the embed so that assitive technology knows when a url retrieve has been succesfull. Added labels for the controls Preview reload only triggered if the values for height and width change * Added control ids for the link picker * Add French translation * Accessibility: Alerts the user how many results have been returned on a tree search (#10100) * Added support for screeen reader alerts on the embed so that assitive technology knows when a url retrieve has been succesfull. Added labels for the controls Preview reload only triggered if the values for height and width change * Tree search details the number of search items returned * Add French translations * Updated LightInject to v6.4.0 * Remove HtmlSanitizer once more - see #9803 * Also make sure NuGet installs the correct version of the CodePages dependency * Bump version to 8.13 RC * Fixed copy preserving sort order (#10091) * Revert "Updated LightInject to v6.4.0" This reverts commit fc77252ec756cf90bb74e7fbbe6dd6d75cbdacfc. * Revert "Add copy logic to Media Picker (#9957)" This reverts commit f7c032af65cac83182782c758a3ab79c86b92e70. * Reintroduce old constructor to make non-breaking * Update cypress test to make macros in the grid work again * Attributes could be multiple items, test specifically if `Directory` is an attribute * Accessibility: Adding label fors and control ids for the macro picker (#10101) * Added support for screeen reader alerts on the embed so that assitive technology knows when a url retrieve has been succesfull. Added labels for the controls Preview reload only triggered if the values for height and width change * Added support for label fors for the macro picker and also gave the ,acro search box a title * Now displays a count of the matching macros returned. Please note the language file amends shared with #10100 * Removed src-only class for the display of the count of messages * Updating typo * Removed top-margin from switcher icon * Allow KeepAlive controller Ping method to be requested by non local requests (#10126) * Allow KeepAlive controller Ping method to be requested by non local requests and accept head requests * removed unused references * fix csproj Co-authored-by: Mole Co-authored-by: Sebastiaan Janssen Co-authored-by: Justin Shearer Co-authored-by: Bjarke Berg Co-authored-by: Callum Whyte Co-authored-by: Shannon Co-authored-by: Elitsa Marinovska Co-authored-by: patrickdemooij9 Co-authored-by: Bjarne Fyrstenborg Co-authored-by: Michael Latouche Co-authored-by: Nathan Woulfe Co-authored-by: Markus Johansson Co-authored-by: Jeavon Leopold Co-authored-by: Benjamin Carleski Co-authored-by: Owain Williams Co-authored-by: Jesper Löfgren Co-authored-by: Martin Bentancour Co-authored-by: Ronald Barendse Co-authored-by: Andy Butland Co-authored-by: BeardinaSuit Co-authored-by: Mads Rasmussen Co-authored-by: Rachel Breeze Co-authored-by: Dave de Moel Co-authored-by: ric <60885685+ricbrady@users.noreply.github.com> Co-authored-by: Carole Rennie Logan Co-authored-by: Dennis Öhman --- build/NuSpecs/UmbracoCms.Web.nuspec | 2 +- src/SolutionInfo.cs | 4 +- src/Umbraco.Core/Composing/TypeLoader.cs | 115 +++++-- src/Umbraco.Core/ConfigsExtensions.cs | 4 +- .../Configuration/GlobalSettings.cs | 41 ++- src/Umbraco.Core/Constants-AppSettings.cs | 5 + src/Umbraco.Core/Constants-Security.cs | 2 +- src/Umbraco.Core/Constants-SvgSanitizer.cs | 23 -- .../Dashboards/ContentDashboardSettings.cs | 24 ++ .../Dashboards/IContentDashboardSettings.cs | 14 + src/Umbraco.Core/IO/IOHelper.cs | 5 +- src/Umbraco.Core/Mapping/UmbracoMapper.cs | 38 ++- .../Implement/UpgradeCheckRepository.cs | 5 + src/Umbraco.Core/Runtime/MainDom.cs | 9 +- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 108 +++--- src/Umbraco.Core/Scoping/Scope.cs | 313 +++++++++--------- src/Umbraco.Core/Scoping/ScopeProvider.cs | 4 + .../Services/Implement/ContentService.cs | 10 +- src/Umbraco.Core/StringExtensions.cs | 41 ++- src/Umbraco.Core/Umbraco.Core.csproj | 3 +- src/Umbraco.Examine/UmbracoExamineIndex.cs | 21 +- .../Compose/ModelsBuilderComposer.cs | 35 +- .../cypress/integration/Content/content.ts | 204 +++++++++++- .../cypress/integration/Settings/languages.ts | 5 +- .../cypress/integration/Settings/templates.ts | 8 +- .../integration/Tour/backofficeTour.ts | 2 +- src/Umbraco.Tests.AcceptanceTest/package.json | 4 +- src/Umbraco.Tests/Mapping/MappingTests.cs | 37 ++- src/Umbraco.Tests/Scoping/ScopeTests.cs | 114 +++++++ src/Umbraco.Tests/Scoping/ScopeUnitTests.cs | 294 +++++++++++++--- .../Services/ContentServiceTests.cs | 26 ++ .../Importing/StandardMvc-Package.xml | 6 +- .../Services/PerformanceTests.cs | 4 +- .../Services/ThreadSafetyServiceTest.cs | 22 +- .../Testing/TestingTests/MockTests.cs | 18 +- .../application/umblogin.directive.js | 6 +- .../application/umbpasswordtip.directive.js | 71 ++++ .../umbvariantcontenteditors.directive.js | 3 + .../forms/prevententersubmit.directive.js | 2 +- .../forms/umbfocuslock.directive.js | 4 - .../forms/umbsearchfilter.directive.js | 10 +- .../components/tabs/umbtabsnav.directive.js | 43 +-- .../tree/umbtreesearchbox.directive.js | 1 + .../users/changepassword.directive.js | 9 + .../mocks/services/localization.mocks.js | 6 +- .../common/resources/contenttype.resource.js | 8 +- .../common/resources/mediatype.resource.js | 4 +- .../common/resources/membertype.resource.js | 25 +- .../common/services/listviewhelper.service.js | 4 +- .../services/umbrequesthelper.service.js | 4 +- .../src/installer/installer.service.js | 6 +- .../src/installer/steps/upgrade.html | 2 +- .../src/less/application/umb-outline.less | 1 + .../src/less/buttons.less | 17 +- .../editor/umb-variant-switcher.less | 1 - .../less/components/umb-search-filter.less | 3 +- src/Umbraco.Web.UI.Client/src/less/forms.less | 17 +- src/Umbraco.Web.UI.Client/src/less/main.less | 4 + src/Umbraco.Web.UI.Client/src/less/panel.less | 1 + .../src/less/property-editors.less | 17 +- .../linkpicker/linkpicker.html | 67 ++-- .../macropicker/macropicker.controller.js | 31 +- .../macropicker/macropicker.html | 24 +- .../userpicker/userpicker.controller.js | 16 +- .../components/application/umb-login.html | 3 +- .../editor/umb-editor-content-header.html | 1 + .../components/forms/umb-search-filter.html | 31 +- .../components/tree/umb-tree-search-box.html | 1 + .../tree/umb-tree-search-results.html | 49 ++- .../components/users/change-password.html | 4 +- .../dashboard/media/mediadashboardvideos.html | 2 +- .../settings/examinemanagementresults.html | 4 +- .../src/views/documenttypes/copy.html | 2 +- .../src/views/documenttypes/export.html | 4 +- .../documenttypes/importdocumenttype.html | 6 +- .../src/views/documenttypes/move.html | 2 +- .../src/views/mediatypes/copy.html | 2 +- .../src/views/mediatypes/move.html | 2 +- .../src/views/membertypes/copy.controller.js | 61 ++++ .../src/views/membertypes/copy.html | 53 +++ .../changepassword.controller.js | 3 + .../datepicker/datepicker.html | 10 +- .../views/propertyeditors/email/email.html | 5 +- .../grid/editors/media.controller.js | 40 +-- .../propertyeditors/grid/grid.controller.js | 4 +- .../propertyeditors/integer/integer.html | 9 +- .../listview/includeproperties.prevalues.html | 4 +- .../listview/layouts.prevalues.html | 18 +- .../propertyeditors/listview/listview.html | 1 - .../userpicker/userpicker.controller.js | 29 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 5 +- .../Umbraco/Views/Default.cshtml | 7 +- src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml | 10 +- src/Umbraco.Web.UI/Umbraco/config/lang/cy.xml | 12 +- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 21 +- src/Umbraco.Web.UI/Umbraco/config/lang/de.xml | 12 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 31 +- .../Umbraco/config/lang/en_us.xml | 31 +- src/Umbraco.Web.UI/Umbraco/config/lang/es.xml | 10 +- src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml | 31 +- src/Umbraco.Web.UI/Umbraco/config/lang/he.xml | 4 +- src/Umbraco.Web.UI/Umbraco/config/lang/it.xml | 4 +- src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml | 4 +- src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml | 4 +- src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml | 4 +- src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml | 6 +- src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml | 4 +- src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml | 14 +- src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml | 6 +- src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml | 24 +- src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml | 6 +- .../Umbraco/config/lang/zh_tw.xml | 6 +- .../config/splashes/noNodes.aspx | 4 +- src/Umbraco.Web.UI/web.Template.config | 1 + .../Dashboards/ContentDashboard.cs | 59 +++- .../Editors/BackOfficeServerVariables.cs | 6 +- .../Binders/ContentModelBinderHelper.cs | 9 +- src/Umbraco.Web/Editors/CodeFileController.cs | 5 +- .../Editors/DashboardController.cs | 4 +- .../Editors/ExamineManagementController.cs | 13 +- .../Editors/KeepAliveController.cs | 4 +- .../Editors/MemberTypeController.cs | 12 + .../Checks/Security/ExcessiveHeadersCheck.cs | 4 +- .../HealthCheck/Checks/Security/HttpsCheck.cs | 21 +- .../Models/ContentEditing/SearchResult.cs | 2 +- .../PropertyEditors/GridPropertyEditor.cs | 4 +- .../PropertyEditors/ListViewConfiguration.cs | 2 +- src/Umbraco.Web/PublishedContentExtensions.cs | 90 ++++- src/Umbraco.Web/PublishedPropertyExtension.cs | 33 +- src/Umbraco.Web/Routing/PublishedRouter.cs | 58 ++-- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 10 - .../Scheduling/BackgroundTaskRunner.cs | 19 +- src/Umbraco.Web/Services/IconService.cs | 8 +- src/Umbraco.Web/Trees/MemberTreeController.cs | 10 +- .../MemberTypeAndGroupTreeControllerBase.cs | 12 +- .../Trees/MemberTypeTreeController.cs | 1 + src/Umbraco.Web/Umbraco.Web.csproj | 3 - 137 files changed, 2127 insertions(+), 850 deletions(-) delete mode 100644 src/Umbraco.Core/Constants-SvgSanitizer.cs create mode 100644 src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs create mode 100644 src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbpasswordtip.directive.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/membertypes/copy.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/membertypes/copy.html diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index de634b4884ce..46165df08776 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -42,7 +42,7 @@ - + diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index c064920d3486..2a7386cb4528 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.12.0")] -[assembly: AssemblyInformationalVersion("8.12.0")] +[assembly: AssemblyFileVersion("8.13.0")] +[assembly: AssemblyInformationalVersion("8.13.0-rc")] diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs index f5c75ff60726..bee6436cd686 100644 --- a/src/Umbraco.Core/Composing/TypeLoader.cs +++ b/src/Umbraco.Core/Composing/TypeLoader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -45,7 +45,7 @@ public class TypeLoader private IEnumerable _assemblies; private bool _reportedChange; private readonly string _localTempPath; - private string _fileBasePath; + private readonly Lazy _fileBasePath; /// /// Initializes a new instance of the class. @@ -70,6 +70,8 @@ internal TypeLoader(IAppPolicyCache runtimeCache, string localTempPath, IProfili _localTempPath = localTempPath; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _fileBasePath = new Lazy(GetFileBasePath); + if (detectChanges) { //first check if the cached hash is string.Empty, if it is then we need @@ -160,7 +162,8 @@ internal string CachedAssembliesHash return _cachedAssembliesHash; var typesHashFilePath = GetTypesHashFilePath(); - if (!File.Exists(typesHashFilePath)) return string.Empty; + if (!File.Exists(typesHashFilePath)) + return string.Empty; var hash = File.ReadAllText(typesHashFilePath, Encoding.UTF8); @@ -339,7 +342,9 @@ internal Dictionary, IEnumerable> ReadCache() var typesListFilePath = GetTypesListFilePath(); if (File.Exists(typesListFilePath) == false) + { return cache; + } using (var stream = GetFileStream(typesListFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, ListFileOpenReadTimeout)) using (var reader = new StreamReader(stream)) @@ -347,11 +352,21 @@ internal Dictionary, IEnumerable> ReadCache() while (true) { var baseType = reader.ReadLine(); - if (baseType == null) return cache; // exit - if (baseType.StartsWith("<")) break; // old xml + if (baseType == null) + { + return cache; // exit + } + + if (baseType.StartsWith("<")) + { + break; // old xml + } var attributeType = reader.ReadLine(); - if (attributeType == null) break; + if (attributeType == null) + { + break; + } var types = new List(); while (true) @@ -370,7 +385,10 @@ internal Dictionary, IEnumerable> ReadCache() types.Add(type); } - if (types == null) break; + if (types == null) + { + break; + } } } @@ -379,28 +397,31 @@ internal Dictionary, IEnumerable> ReadCache() } // internal for tests - internal string GetTypesListFilePath() => GetFileBasePath() + ".list"; + internal string GetTypesListFilePath() => _fileBasePath.Value + ".list"; - private string GetTypesHashFilePath() => GetFileBasePath() + ".hash"; + private string GetTypesHashFilePath() => _fileBasePath.Value + ".hash"; + /// + /// Used to produce the Lazy value of _fileBasePath + /// + /// private string GetFileBasePath() { - lock (_locko) - { - if (_fileBasePath != null) - return _fileBasePath; - - _fileBasePath = Path.Combine(_localTempPath, "TypesCache", "umbraco-types." + NetworkHelper.FileSafeMachineName); + var fileBasePath = Path.Combine(_localTempPath, "TypesCache", "umbraco-types." + NetworkHelper.FileSafeMachineName); - // ensure that the folder exists - var directory = Path.GetDirectoryName(_fileBasePath); - if (directory == null) - throw new InvalidOperationException($"Could not determine folder for path \"{_fileBasePath}\"."); - if (Directory.Exists(directory) == false) - Directory.CreateDirectory(directory); + // ensure that the folder exists + var directory = Path.GetDirectoryName(fileBasePath); + if (directory == null) + { + throw new InvalidOperationException($"Could not determine folder for path \"{fileBasePath}\"."); + } - return _fileBasePath; + if (Directory.Exists(directory) == false) + { + Directory.CreateDirectory(directory); } + + return fileBasePath; } // internal for tests @@ -416,7 +437,10 @@ internal void WriteCache() writer.WriteLine(typeList.BaseType == null ? string.Empty : typeList.BaseType.FullName); writer.WriteLine(typeList.AttributeType == null ? string.Empty : typeList.AttributeType.FullName); foreach (var type in typeList.Types) + { writer.WriteLine(type.AssemblyQualifiedName); + } + writer.WriteLine(); } } @@ -434,16 +458,22 @@ void TimerRelease(object o) WriteCache(); } catch { /* bah - just don't die */ } - if (!_timing) _timer = null; + if (!_timing) + _timer = null; } } lock (_timerLock) { if (_timer == null) + { _timer = new Timer(TimerRelease, null, ListFileWriteThrottle, Timeout.Infinite); + } else + { _timer.Change(ListFileWriteThrottle, Timeout.Infinite); + } + _timing = true; } } @@ -476,7 +506,9 @@ private Stream GetFileStream(string path, FileMode fileMode, FileAccess fileAcce catch { if (--attempts == 0) + { throw; + } _logger.Debug("Attempted to get filestream for file {Path} failed, {NumberOfAttempts} attempts left, pausing for {PauseMilliseconds} milliseconds", path, attempts, pauseMilliseconds); Thread.Sleep(pauseMilliseconds); @@ -543,7 +575,8 @@ public IEnumerable GetAssemblyAttributes() /// attributeTypes public IEnumerable GetAssemblyAttributes(params Type[] attributeTypes) { - if (attributeTypes == null) throw new ArgumentNullException(nameof(attributeTypes)); + if (attributeTypes == null) + throw new ArgumentNullException(nameof(attributeTypes)); return AssembliesToScan.SelectMany(a => attributeTypes.SelectMany(at => a.GetCustomAttributes(at))).ToList(); } @@ -563,7 +596,9 @@ public IEnumerable GetAssemblyAttributes(params Type[] attributeTypes public IEnumerable GetTypes(bool cache = true, IEnumerable specificAssemblies = null) { if (_logger == null) + { throw new InvalidOperationException("Cannot get types from a test/blank type loader."); + } // do not cache anything from specific assemblies cache &= specificAssemblies == null; @@ -583,7 +618,7 @@ public IEnumerable GetTypes(bool cache = true, IEnumerable sp // get IDiscoverable and always cache var discovered = GetTypesInternal( - typeof (IDiscoverable), null, + typeof(IDiscoverable), null, () => TypeFinder.FindClassesOfType(AssembliesToScan), "scanning assemblies", true); @@ -594,9 +629,9 @@ public IEnumerable GetTypes(bool cache = true, IEnumerable sp // filter the cached discovered types (and maybe cache the result) return GetTypesInternal( - typeof (T), null, + typeof(T), null, () => discovered - .Where(x => typeof (T).IsAssignableFrom(x)), + .Where(x => typeof(T).IsAssignableFrom(x)), "filtering IDiscoverable", cache); } @@ -614,7 +649,9 @@ public IEnumerable GetTypesWithAttribute(bool cache = true, where TAttribute : Attribute { if (_logger == null) + { throw new InvalidOperationException("Cannot get types from a test/blank type loader."); + } // do not cache anything from specific assemblies cache &= specificAssemblies == null; @@ -633,7 +670,7 @@ public IEnumerable GetTypesWithAttribute(bool cache = true, // get IDiscoverable and always cache var discovered = GetTypesInternal( - typeof (IDiscoverable), null, + typeof(IDiscoverable), null, () => TypeFinder.FindClassesOfType(AssembliesToScan), "scanning assemblies", true); @@ -644,7 +681,7 @@ public IEnumerable GetTypesWithAttribute(bool cache = true, // filter the cached discovered types (and maybe cache the result) return GetTypesInternal( - typeof (T), typeof (TAttribute), + typeof(T), typeof(TAttribute), () => discovered .Where(x => typeof(T).IsAssignableFrom(x)) .Where(x => x.GetCustomAttributes(false).Any()), @@ -664,7 +701,9 @@ public IEnumerable GetAttributedTypes(bool cache = true, IEnum where TAttribute : Attribute { if (_logger == null) + { throw new InvalidOperationException("Cannot get types from a test/blank type loader."); + } // do not cache anything from specific assemblies cache &= specificAssemblies == null; @@ -673,7 +712,7 @@ public IEnumerable GetAttributedTypes(bool cache = true, IEnum _logger.Debug("Running a full, non-cached, scan for types / attribute {AttributeName} (slow).", typeof(TAttribute).FullName); return GetTypesInternal( - typeof (object), typeof (TAttribute), + typeof(object), typeof(TAttribute), () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), "scanning assemblies", cache); @@ -693,12 +732,14 @@ private IEnumerable GetTypesInternal( var name = GetName(baseType, attributeType); lock (_locko) - using (_logger.DebugDuration( + { + using (_logger.DebugDuration( "Getting " + name, "Got " + name)) // cannot contain typesFound.Count as it's evaluated before the find - { - // get within a lock & timer - return GetTypesInternalLocked(baseType, attributeType, finder, action, cache); + { + // get within a lock & timer + return GetTypesInternalLocked(baseType, attributeType, finder, action, cache); + } } } @@ -720,7 +761,9 @@ private IEnumerable GetTypesInternalLocked( var listKey = new CompositeTypeTypeKey(baseType ?? tobject, attributeType ?? tobject); TypeList typeList = null; if (cache) + { _types.TryGetValue(listKey, out typeList); // else null + } // if caching and found, return if (typeList != null) @@ -795,7 +838,9 @@ private IEnumerable GetTypesInternalLocked( _logger.Debug("Getting {TypeName}: " + action + ".", GetName(baseType, attributeType)); foreach (var t in finder()) + { typeList.Add(t); + } } // if we are to cache the results, do so @@ -807,7 +852,9 @@ private IEnumerable GetTypesInternalLocked( _types[listKey] = typeList; //if we are scanning then update the cache file if (scan) + { UpdateCache(); + } } _logger.Debug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant()); diff --git a/src/Umbraco.Core/ConfigsExtensions.cs b/src/Umbraco.Core/ConfigsExtensions.cs index d1672c6c7ff6..10594fc97028 100644 --- a/src/Umbraco.Core/ConfigsExtensions.cs +++ b/src/Umbraco.Core/ConfigsExtensions.cs @@ -1,10 +1,10 @@ using System.IO; using Umbraco.Core.Cache; -using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Grid; using Umbraco.Core.Configuration.HealthChecks; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Dashboards; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; @@ -48,6 +48,8 @@ public static void AddCoreConfigs(this Configs configs) configDir, factory.GetInstance(), factory.GetInstance().Debug)); + + configs.Add(() => new ContentDashboardSettings()); } } } diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index b7dce212855a..c844abe75e49 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -395,7 +395,6 @@ public bool UseHttps } } - /// /// An int value representing the time in milliseconds to lock the database for a write operation /// @@ -409,26 +408,34 @@ public int SqlWriteLockTimeOut { if (_sqlWriteLockTimeOut != default) return _sqlWriteLockTimeOut; - var timeOut = 5000; // 5 seconds - var appSettingSqlWriteLockTimeOut = ConfigurationManager.AppSettings[Constants.AppSettings.SqlWriteLockTimeOut]; - if(int.TryParse(appSettingSqlWriteLockTimeOut, out var configuredTimeOut)) - { - // Only apply this setting if it's not excessively high or low - const int minimumTimeOut = 100; - const int maximumTimeOut = 20000; - if (configuredTimeOut >= minimumTimeOut && configuredTimeOut <= maximumTimeOut) // between 0.1 and 20 seconds - { - timeOut = configuredTimeOut; - } - else - { - Current.Logger.Warn($"The `{Constants.AppSettings.SqlWriteLockTimeOut}` setting in web.config is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms, defaulting back to {timeOut}"); - } - } + var timeOut = GetSqlWriteLockTimeoutFromConfigFile(Current.Logger); _sqlWriteLockTimeOut = timeOut; return _sqlWriteLockTimeOut; } } + + internal static int GetSqlWriteLockTimeoutFromConfigFile(ILogger logger) + { + var timeOut = 5000; // 5 seconds + var appSettingSqlWriteLockTimeOut = ConfigurationManager.AppSettings[Constants.AppSettings.SqlWriteLockTimeOut]; + if (int.TryParse(appSettingSqlWriteLockTimeOut, out var configuredTimeOut)) + { + // Only apply this setting if it's not excessively high or low + const int minimumTimeOut = 100; + const int maximumTimeOut = 20000; + if (configuredTimeOut >= minimumTimeOut && configuredTimeOut <= maximumTimeOut) // between 0.1 and 20 seconds + { + timeOut = configuredTimeOut; + } + else + { + logger.Warn( + $"The `{Constants.AppSettings.SqlWriteLockTimeOut}` setting in web.config is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms, defaulting back to {timeOut}"); + } + } + + return timeOut; + } } } diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index f04f0e1f5fee..1f096ab9f99f 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -110,6 +110,11 @@ public static class AppSettings /// public const string UseHttps = "Umbraco.Core.UseHttps"; + /// + /// A true/false value indicating whether the content dashboard should be visible for all user groups. + /// + public const string AllowContentDashboardAccessToAllUsers = "Umbraco.Core.AllowContentDashboardAccessToAllUsers"; + /// /// TODO: FILL ME IN /// diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index f900288ef54d..2b6244debb90 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -25,7 +25,7 @@ public static class Security /// /// The name of the 'unknown' user. /// - public const string UnknownUserName = "SYTEM"; + public const string UnknownUserName = "SYSTEM"; public const string AdminGroupAlias = "admin"; public const string EditorGroupAlias = "editor"; diff --git a/src/Umbraco.Core/Constants-SvgSanitizer.cs b/src/Umbraco.Core/Constants-SvgSanitizer.cs deleted file mode 100644 index c92b9f56c72e..000000000000 --- a/src/Umbraco.Core/Constants-SvgSanitizer.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; - -namespace Umbraco.Core -{ - public static partial class Constants - { - /// - /// Defines the alias identifiers for Umbraco's core application sections. - /// - public static class SvgSanitizer - { - /// - /// Allowlist for SVG attributes. - /// - public static readonly IList Attributes = new [] { "accent-height", "accumulate", "additive", "alignment-baseline", "allowReorder", "alphabetic", "amplitude", "arabic-form", "ascent", "attributeName", "attributeType", "autoReverse", "azimuth", "baseFrequency", "baseline-shift", "baseProfile", "bbox", "begin", "bias", "by", "calcMode", "cap-height", "class", "clip", "clipPathUnits", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "contentScriptType", "contentStyleType", "cursor", "cx", "cy", "d", "decelerate", "descent", "diffuseConstant", "direction", "display", "divisor", "dominant-baseline", "dur", "dx", "dy", "edgeMode", "elevation", "enable-background", "end", "exponent", "externalResourcesRequired", "Section", "fill", "fill-opacity", "fill-rule", "filter", "filterRes", "filterUnits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "format", "from", "fr", "fx", "fy", "g1", "g2", "glyph-name", "glyph-orientation-horizontal", "glyph-orientation-vertical", "glyphRef", "gradientTransform", "gradientUnits", "hanging", "height", "href", "hreflang", "horiz-adv-x", "horiz-origin-x", "ISection", "id", "ideographic", "image-rendering", "in", "in2", "intercept", "k", "k1", "k2", "k3", "k4", "kernelMatrix", "kernelUnitLength", "kerning", "keyPoints", "keySplines", "keyTimes", "lang", "lengthAdjust", "letter-spacing", "lighting-color", "limitingConeAngle", "local", "MSection", "marker-end", "marker-mid", "marker-start", "markerHeight", "markerUnits", "markerWidth", "mask", "maskContentUnits", "maskUnits", "mathematical", "max", "media", "method", "min", "mode", "NSection", "name", "numOctaves", "offset", "opacity", "operator", "order", "orient", "orientation", "origin", "overflow", "overline-position", "overline-thickness", "panose-1", "paint-order", "path", "pathLength", "patternContentUnits", "patternTransform", "patternUnits", "ping", "pointer-events", "points", "pointsAtX", "pointsAtY", "pointsAtZ", "preserveAlpha", "preserveAspectRatio", "primitiveUnits", "r", "radius", "referrerPolicy", "refX", "refY", "rel", "rendering-intent", "repeatCount", "repeatDur", "requiredExtensions", "requiredFeatures", "restart", "result", "rotate", "rx", "ry", "scale", "seed", "shape-rendering", "slope", "spacing", "specularConstant", "specularExponent", "speed", "spreadMethod", "startOffset", "stdDeviation", "stemh", "stemv", "stitchTiles", "stop-color", "stop-opacity", "strikethrough-position", "strikethrough-thickness", "string", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "surfaceScale", "systemLanguage", "tabindex", "tableValues", "target", "targetX", "targetY", "text-anchor", "text-decoration", "text-rendering", "textLength", "to", "transform", "type", "u1", "u2", "underline-position", "underline-thickness", "unicode", "unicode-bidi", "unicode-range", "units-per-em", "v-alphabetic", "v-hanging", "v-ideographic", "v-mathematical", "values", "vector-effect", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "viewBox", "viewTarget", "visibility", "width", "widths", "word-spacing", "writing-mode", "x", "x-height", "x1", "x2", "xChannelSelector", "xlink:actuate", "xlink:arcrole", "xlink:href", "xlink:role", "xlink:show", "xlink:title", "xlink:type", "xml:base", "xml:lang", "xml:space", "y", "y1", "y2", "yChannelSelector", "z", "zoomAndPan" }; - - /// - /// Allowlist for SVG tabs. - /// - public static readonly IList Tags = new [] { "a", "altGlyph", "altGlyphDef", "altGlyphItem", "animate", "animateColor", "animateMotion", "animateTransform", "circle", "clipPath", "color-profile", "cursor", "defs", "desc", "discard", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "font", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignObject", "g", "glyph", "glyphRef", "hatch", "hatchpath", "hkern", "image", "line", "linearGradient", "marker", "mask", "mesh", "meshgradient", "meshpatch", "meshrow", "metadata", "missing-glyph", "mpath", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "set", "solidcolor", "stop", "svg", "switch", "symbol", "text", "textPath", "title", "tref", "tspan", "unknown", "use", "view", "vkern" }; - } - } -} diff --git a/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs b/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs new file mode 100644 index 000000000000..f8fb5c7b0644 --- /dev/null +++ b/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs @@ -0,0 +1,24 @@ +using System.Configuration; + +namespace Umbraco.Core.Dashboards +{ + public class ContentDashboardSettings: IContentDashboardSettings + { + + /// + /// Gets a value indicating whether the content dashboard should be available to all users. + /// + /// + /// true if the dashboard is visible for all user groups; otherwise, false + /// and the default access rules for that dashboard will be in use. + /// + public bool AllowContentDashboardAccessToAllUsers + { + get + { + bool.TryParse(ConfigurationManager.AppSettings[Constants.AppSettings.AllowContentDashboardAccessToAllUsers], out var value); + return value; + } + } + } +} diff --git a/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs b/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs new file mode 100644 index 000000000000..862a28b90e55 --- /dev/null +++ b/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Core.Dashboards +{ + public interface IContentDashboardSettings + { + /// + /// Gets a value indicating whether the content dashboard should be available to all users. + /// + /// + /// true if the dashboard is visible for all user groups; otherwise, false + /// and the default access rules for that dashboard will be in use. + /// + bool AllowContentDashboardAccessToAllUsers { get; } + } +} diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index 8661f73fb1d4..69ce50de9cc1 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -81,6 +81,7 @@ public static Attempt TryResolveUrl(string virtualPath) public static string MapPath(string path, bool useHttpContext) { if (path == null) throw new ArgumentNullException("path"); + useHttpContext = useHttpContext && IsHosted; // Check if the path is already mapped @@ -89,10 +90,8 @@ public static string MapPath(string path, bool useHttpContext) { return path; } - // Check that we even have an HttpContext! otherwise things will fail anyways - // http://umbraco.codeplex.com/workitem/30946 - if (useHttpContext && HttpContext.Current != null) + if (useHttpContext) { //string retval; if (String.IsNullOrEmpty(path) == false && (path.StartsWith("~") || path.StartsWith(SystemDirectories.Root))) diff --git a/src/Umbraco.Core/Mapping/UmbracoMapper.cs b/src/Umbraco.Core/Mapping/UmbracoMapper.cs index e62825101c4f..36e3f9eab93b 100644 --- a/src/Umbraco.Core/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Core/Mapping/UmbracoMapper.cs @@ -3,7 +3,9 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Composing; using Umbraco.Core.Exceptions; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Mapping { @@ -42,16 +44,29 @@ private readonly ConcurrentDictionary>> _maps = new ConcurrentDictionary>>(); + private readonly IScopeProvider _scopeProvider; + /// /// Initializes a new instance of the class. /// /// - public UmbracoMapper(MapDefinitionCollection profiles) + /// + public UmbracoMapper(MapDefinitionCollection profiles, IScopeProvider scopeProvider) { + _scopeProvider = scopeProvider; + foreach (var profile in profiles) profile.DefineMaps(this); } + /// + /// Initializes a new instance of the class. + /// + /// + [Obsolete("This constructor is no longer used and will be removed in future versions, use the other constructor instead")] + public UmbracoMapper(MapDefinitionCollection profiles) : this(profiles, Current.ScopeProvider) + {} + #region Define private static TTarget ThrowCtor(TSource source, MapperContext context) @@ -203,7 +218,10 @@ private TTarget Map(object source, Type sourceType, MapperContext conte if (ctor != null && map != null) { var target = ctor(source, context); - map(source, target, context); + using (var scope = _scopeProvider.CreateScope(autoComplete: true)) + { + map(source, target, context); + } return (TTarget)target; } @@ -248,11 +266,14 @@ private TTarget MapEnumerableInternal(IEnumerable source, Type targetGe { var targetList = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(targetGenericArg)); - foreach (var sourceItem in source) + using (var scope = _scopeProvider.CreateScope(autoComplete: true)) { - var targetItem = ctor(sourceItem, context); - map(sourceItem, targetItem, context); - targetList.Add(targetItem); + foreach (var sourceItem in source) + { + var targetItem = ctor(sourceItem, context); + map(sourceItem, targetItem, context); + targetList.Add(targetItem); + } } object target = targetList; @@ -315,7 +336,10 @@ public TTarget Map(TSource source, TTarget target, MapperConte // if there is a direct map, map if (map != null) { - map(source, target, context); + using (var scope = _scopeProvider.CreateScope(autoComplete: true)) + { + map(source, target, context); + } return target; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs index 365e8ba48145..95f699d95245 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs @@ -24,6 +24,11 @@ public async Task CheckUpgradeAsync(SemVersion version) return result ?? new UpgradeResult("None", "", ""); } + catch (UnsupportedMediaTypeException) + { + // this occurs if the server for Our is up but doesn't return a valid result (ex. content type) + return new UpgradeResult("None", "", ""); + } catch (HttpRequestException) { // this occurs if the server for Our is down or cannot be reached diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index d93084128920..d784560f2c04 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -179,7 +179,14 @@ private bool Acquire() _listenTask = _mainDomLock.ListenAsync(); _listenCompleteTask = _listenTask.ContinueWith(t => { - _logger.Debug("Listening task completed with {TaskStatus}", _listenTask.Status); + if (_listenTask.Exception != null) + { + _logger.Warn("Listening task completed with {TaskStatus}, Exception: {Exception}", _listenTask.Status, _listenTask.Exception); + } + else + { + _logger.Debug("Listening task completed with {TaskStatus}", _listenTask.Status); + } OnSignal("signal"); }, TaskScheduler.Default); // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index d98d62cb2038..12359c96d102 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -1,5 +1,6 @@ using NPoco; using System; +using System.Configuration; using System.Data; using System.Data.SqlClient; using System.Diagnostics; @@ -7,6 +8,7 @@ using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; +using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -18,6 +20,7 @@ namespace Umbraco.Core.Runtime { internal class SqlMainDomLock : IMainDomLock { + private readonly TimeSpan _lockTimeout; private string _lockId; private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom"; private const string UpdatedSuffix = "_updated"; @@ -40,6 +43,8 @@ public SqlMainDomLock(ILogger logger) Constants.System.UmbracoConnectionName, _logger, new Lazy(() => new MapperCollection(Enumerable.Empty()))); + + _lockTimeout = TimeSpan.FromMilliseconds(GlobalSettings.GetSqlWriteLockTimeoutFromConfigFile(logger)); } public async Task AcquireLockAsync(int millisecondsTimeout) @@ -81,7 +86,7 @@ public async Task AcquireLockAsync(int millisecondsTimeout) // wait to get a write lock _sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom); } - catch(SqlException ex) + catch (SqlException ex) { if (IsLockTimeoutException(ex)) { @@ -121,7 +126,7 @@ public async Task AcquireLockAsync(int millisecondsTimeout) } - return await WaitForExistingAsync(tempId, millisecondsTimeout); + return await WaitForExistingAsync(tempId, millisecondsTimeout).ConfigureAwait(false); } public Task ListenAsync() @@ -134,13 +139,15 @@ public Task ListenAsync() // Create a long running task (dedicated thread) // to poll to check if we are still the MainDom registered in the DB - return Task.Factory.StartNew( - ListeningLoop, - _cancellationTokenSource.Token, - TaskCreationOptions.LongRunning, - // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html - TaskScheduler.Default); - + using (ExecutionContext.SuppressFlow()) + { + return Task.Factory.StartNew( + ListeningLoop, + _cancellationTokenSource.Token, + TaskCreationOptions.LongRunning, + // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html + TaskScheduler.Default); + } } /// @@ -198,7 +205,7 @@ private void ListeningLoop() db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.ReadLock(db, _lockTimeout, Constants.Locks.MainDom); if (!IsMainDomValue(_lockId, db)) { @@ -222,11 +229,29 @@ private void ListeningLoop() } finally { - db?.CompleteTransaction(); - db?.Dispose(); + // Even if any of the above fail like BeginTransaction, or even a query after the + // Transaction is started, the calls below will not throw. I've tried all sorts of + // combinations to see if I can make this throw but I can't. In any case, we'll be + // extra safe and try/catch/log + try + { + db?.CompleteTransaction(); + } + catch (Exception ex) + { + _logger.Error(ex, "Unexpected error completing transaction."); + } + + try + { + db?.Dispose(); + } + catch (Exception ex) + { + _logger.Error(ex, "Unexpected error completing disposing."); + } } } - } } @@ -240,37 +265,40 @@ private Task WaitForExistingAsync(string tempId, int millisecondsTimeout) { var updatedTempId = tempId + UpdatedSuffix; - return Task.Run(() => + using (ExecutionContext.SuppressFlow()) { - try + return Task.Run(() => { - using var db = _dbFactory.CreateDatabase(); - - var watch = new Stopwatch(); - watch.Start(); - while (true) + try { - // poll very often, we need to take over as fast as we can - // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO - Thread.Sleep(1000); - - var acquired = TryAcquire(db, tempId, updatedTempId); - if (acquired.HasValue) - return acquired.Value; + using var db = _dbFactory.CreateDatabase(); - if (watch.ElapsedMilliseconds >= millisecondsTimeout) + var watch = new Stopwatch(); + watch.Start(); + while (true) { - return AcquireWhenMaxWaitTimeElapsed(db); + // poll very often, we need to take over as fast as we can + // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO + Thread.Sleep(1000); + + var acquired = TryAcquire(db, tempId, updatedTempId); + if (acquired.HasValue) + return acquired.Value; + + if (watch.ElapsedMilliseconds >= millisecondsTimeout) + { + return AcquireWhenMaxWaitTimeElapsed(db); + } } } - } - catch (Exception ex) - { - _logger.Error(ex, "An error occurred trying to acquire and waiting for existing SqlMainDomLock to shutdown"); - return false; - } + catch (Exception ex) + { + _logger.Error(ex, "An error occurred trying to acquire and waiting for existing SqlMainDomLock to shutdown"); + return false; + } - }, _cancellationTokenSource.Token); + }, _cancellationTokenSource.Token); + } } private bool? TryAcquire(IUmbracoDatabase db, string tempId, string updatedTempId) @@ -284,7 +312,7 @@ private Task WaitForExistingAsync(string tempId, int millisecondsTimeout) { transaction = db.GetTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.ReadLock(db, _lockTimeout, Constants.Locks.MainDom); // the row var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); @@ -296,7 +324,7 @@ private Task WaitForExistingAsync(string tempId, int millisecondsTimeout) // which indicates that we // can acquire it and it has shutdown. - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, _lockTimeout, Constants.Locks.MainDom); // so now we update the row with our appdomain id InsertLockRecord(_lockId, db); @@ -355,7 +383,7 @@ private bool AcquireWhenMaxWaitTimeElapsed(IUmbracoDatabase db) { transaction = db.GetTransaction(IsolationLevel.ReadCommitted); - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, _lockTimeout, Constants.Locks.MainDom); // so now we update the row with our appdomain id InsertLockRecord(_lockId, db); @@ -438,7 +466,7 @@ protected virtual void Dispose(bool disposing) db.BeginTransaction(IsolationLevel.ReadCommitted); // get a write lock - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, _lockTimeout, Constants.Locks.MainDom); // When we are disposed, it means we have released the MainDom lock // and called all MainDom release callbacks, in this case diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index c4c8d086225a..4c058cbdb763 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data; +using System.Text; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Events; @@ -36,11 +37,10 @@ internal class Scope : IScope2 private IEventDispatcher _eventDispatcher; private object _dictionaryLocker; - - // ReadLocks and WriteLocks if we're the outer most scope it's those owned by the entire chain - // If we're a child scope it's those that we have requested. - internal readonly Dictionary ReadLocks; - internal readonly Dictionary WriteLocks; + private HashSet _readLocks; + private HashSet _writeLocks; + internal Dictionary> ReadLocks; + internal Dictionary> WriteLocks; // initializes a new scope private Scope(ScopeProvider scopeProvider, @@ -67,8 +67,6 @@ private Scope(ScopeProvider scopeProvider, Detachable = detachable; _dictionaryLocker = new object(); - ReadLocks = new Dictionary(); - WriteLocks = new Dictionary(); #if DEBUG_SCOPES _scopeProvider.RegisterScope(this); @@ -345,6 +343,8 @@ public void Dispose() if (this != _scopeProvider.AmbientScope) { + var failedMessage = $"The {nameof(Scope)} {this.InstanceId} being disposed is not the Ambient {nameof(Scope)} {(_scopeProvider.AmbientScope?.InstanceId.ToString() ?? "NULL")}. This typically indicates that a child {nameof(Scope)} was not disposed, or flowed to a child thread that was not awaited, or concurrent threads are accessing the same {nameof(Scope)} (Ambient context) which is not supported. If using Task.Run (or similar) as a fire and forget tasks or to run threads in parallel you must suppress execution context flow with ExecutionContext.SuppressFlow() and ExecutionContext.RestoreFlow()."; + #if DEBUG_SCOPES var ambient = _scopeProvider.AmbientScope; _logger.Debug("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)"); @@ -356,24 +356,21 @@ public void Dispose() + "- ambient ctor ->\r\n" + ambientInfos.CtorStack + "\r\n" + "- dispose ctor ->\r\n" + disposeInfos.CtorStack + "\r\n"); #else - throw new InvalidOperationException("Not the ambient scope."); + throw new InvalidOperationException(failedMessage); #endif } // Decrement the lock counters on the parent if any. - if (ParentScope != null) + ClearLocks(InstanceId); + if (ParentScope is null) { - lock (_dictionaryLocker) + // We're the parent scope, make sure that locks of all scopes has been cleared + // Since we're only reading we don't have to be in a lock + if (ReadLocks?.Count > 0 || WriteLocks?.Count > 0) { - foreach (var readLockPair in ReadLocks) - { - DecrementReadLock(readLockPair.Key, readLockPair.Value); - } - - foreach (var writeLockPair in WriteLocks) - { - DecrementWriteLock(writeLockPair.Key, writeLockPair.Value); - } + var exception = new InvalidOperationException($"All scopes has not been disposed from parent scope: {InstanceId}, see log for more details."); + _logger.Error(exception, GenerateUnclearedScopesLogMessage()); + throw exception; } } @@ -396,6 +393,42 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Generates a log message with all scopes that hasn't cleared their locks, including how many, and what locks they have requested. + /// + /// Log message. + private string GenerateUnclearedScopesLogMessage() + { + // Dump the dicts into a message for the locks. + StringBuilder builder = new StringBuilder(); + builder.AppendLine($"Lock counters aren't empty, suggesting a scope hasn't been properly disposed, parent id: {InstanceId}"); + WriteLockDictionaryToString(ReadLocks, builder, "read locks"); + WriteLockDictionaryToString(WriteLocks, builder, "write locks"); + return builder.ToString(); + } + + /// + /// Writes a locks dictionary to a for logging purposes. + /// + /// Lock dictionary to report on. + /// String builder to write to. + /// The name to report the dictionary as. + private void WriteLockDictionaryToString(Dictionary> dict, StringBuilder builder, string dictName) + { + if (dict?.Count > 0) + { + builder.AppendLine($"Remaining {dictName}:"); + foreach (var instance in dict) + { + builder.AppendLine($"Scope {instance.Key}"); + foreach (var lockCounter in instance.Value) + { + builder.AppendLine($"\tLock ID: {lockCounter.Key} - times requested: {lockCounter.Value}"); + } + } + } + } + private void DisposeLastScope() { // figure out completed @@ -516,207 +549,157 @@ private static void TryFinally(int index, Action[] actions) ?? (_logUncompletedScopes = Current.Configs.CoreDebug().LogUncompletedScopes)).Value; /// - /// Decrements the count of the ReadLocks with a specific lock object identifier we currently hold + /// Increment the counter of a locks dictionary, either ReadLocks or WriteLocks, + /// for a specific scope instance and lock identifier. Must be called within a lock. /// - /// Lock object identifier to decrement - /// Amount to decrement the lock count with - public void DecrementReadLock(int lockId, int amountToDecrement) + /// Lock ID to increment. + /// Instance ID of the scope requesting the lock. + /// Reference to the dictionary to increment on + private void IncrementLock(int lockId, Guid instanceId, ref Dictionary> locks) { - // If we aren't the outermost scope, pass it on to the parent. - if (ParentScope != null) - { - ParentScope.DecrementReadLock(lockId, amountToDecrement); - return; - } + // Since we've already checked that we're the parent in the WriteLockInner method, we don't need to check again. + // If it's the very first time a lock has been requested the WriteLocks dict hasn't been instantiated yet. + locks ??= new Dictionary>(); - lock (_dictionaryLocker) + // Try and get the dict associated with the scope id. + var locksDictFound = locks.TryGetValue(instanceId, out var locksDict); + if (locksDictFound) { - ReadLocks[lockId] -= amountToDecrement; + locksDict.TryGetValue(lockId, out var value); + locksDict[lockId] = value + 1; } - } - - /// - /// Decrements the count of the WriteLocks with a specific lock object identifier we currently hold. - /// - /// Lock object identifier to decrement. - /// Amount to decrement the lock count with - public void DecrementWriteLock(int lockId, int amountToDecrement) - { - // If we aren't the outermost scope, pass it on to the parent. - if (ParentScope != null) - { - ParentScope.DecrementWriteLock(lockId, amountToDecrement); - return; - } - - lock (_dictionaryLocker) + else { - WriteLocks[lockId] -= amountToDecrement; + // The scope hasn't requested a lock yet, so we have to create a dict for it. + locks.Add(instanceId, new Dictionary()); + locks[instanceId][lockId] = 1; } } /// - /// Increment the count of the read locks we've requested + /// Clears all lock counters for a given scope instance, signalling that the scope has been disposed. /// - /// - /// This should only be done on child scopes since it's then used to decrement the count later. - /// - /// - private void IncrementRequestedReadLock(params int[] lockIds) + /// Instance ID of the scope to clear. + private void ClearLocks(Guid instanceId) { - // We need to keep track of what lockIds we have requested locks for to be able to decrement them. - if (ParentScope != null) + if (ParentScope is not null) { - foreach (var lockId in lockIds) - { - lock (_dictionaryLocker) - { - if (ReadLocks.ContainsKey(lockId)) - { - ReadLocks[lockId] += 1; - } - else - { - ReadLocks[lockId] = 1; - } - } - } + ParentScope.ClearLocks(instanceId); } - } - - /// - /// Increment the count of the write locks we've requested - /// - /// - /// This should only be done on child scopes since it's then used to decrement the count later. - /// - /// - private void IncrementRequestedWriteLock(params int[] lockIds) - { - // We need to keep track of what lockIds we have requested locks for to be able to decrement them. - if (ParentScope != null) + else { - foreach (var lockId in lockIds) + lock (_dictionaryLocker) { - lock (_dictionaryLocker) - { - if (WriteLocks.ContainsKey(lockId)) - { - WriteLocks[lockId] += 1; - } - else - { - WriteLocks[lockId] = 1; - } - } + ReadLocks?.Remove(instanceId); + WriteLocks?.Remove(instanceId); } } } /// - public void ReadLock(params int[] lockIds) - { - IncrementRequestedReadLock(lockIds); - ReadLockInner(null, lockIds); - } + public void ReadLock(params int[] lockIds) => ReadLockInner(InstanceId, null, lockIds); /// - public void ReadLock(TimeSpan timeout, int lockId) - { - IncrementRequestedReadLock(lockId); - ReadLockInner(timeout, lockId); - } + public void ReadLock(TimeSpan timeout, int lockId) => ReadLockInner(InstanceId, timeout, lockId); /// - public void WriteLock(params int[] lockIds) - { - IncrementRequestedWriteLock(lockIds); - WriteLockInner(null, lockIds); - } + public void WriteLock(params int[] lockIds) => WriteLockInner(InstanceId, null, lockIds); /// - public void WriteLock(TimeSpan timeout, int lockId) - { - IncrementRequestedWriteLock(lockId); - WriteLockInner(timeout, lockId); - } + public void WriteLock(TimeSpan timeout, int lockId) => WriteLockInner(InstanceId, timeout, lockId); /// /// Handles acquiring a read lock, will delegate it to the parent if there are any. /// + /// Instance ID of the requesting scope. /// Optional database timeout in milliseconds. /// Array of lock object identifiers. - internal void ReadLockInner(TimeSpan? timeout = null, params int[] lockIds) + private void ReadLockInner(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) { - if (ParentScope != null) + if (ParentScope is not null) { - // Delegate acquiring the lock to the parent if any. - ParentScope.ReadLockInner(timeout, lockIds); - return; + // If we have a parent we delegate lock creation to parent. + ParentScope.ReadLockInner(instanceId, timeout, lockIds); } - - // If we are the parent, then handle the lock request. - foreach (var lockId in lockIds) + else { - lock (_dictionaryLocker) - { - // Only acquire the lock if we haven't done so yet. - if (!ReadLocks.ContainsKey(lockId)) - { - if (timeout is null) - { - // We want a lock with a custom timeout - ObtainReadLock(lockId); - } - else - { - // We just want an ordinary lock. - ObtainTimoutReadLock(lockId, timeout.Value); - } - // Add the lockId as a key to the dict. - ReadLocks[lockId] = 0; - } - - ReadLocks[lockId] += 1; - } + // We are the outermost scope, handle the lock request. + LockInner(instanceId, ref ReadLocks, ref _readLocks, ObtainReadLock, ObtainTimeoutReadLock, timeout, lockIds); } } /// /// Handles acquiring a write lock with a specified timeout, will delegate it to the parent if there are any. /// + /// Instance ID of the requesting scope. /// Optional database timeout in milliseconds. /// Array of lock object identifiers. - internal void WriteLockInner(TimeSpan? timeout = null, params int[] lockIds) + private void WriteLockInner(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) { - if (ParentScope != null) + if (ParentScope is not null) { // If we have a parent we delegate lock creation to parent. - ParentScope.WriteLockInner(timeout, lockIds); - return; + ParentScope.WriteLockInner(instanceId, timeout, lockIds); + } + else + { + // We are the outermost scope, handle the lock request. + LockInner(instanceId, ref WriteLocks, ref _writeLocks, ObtainWriteLock, ObtainTimeoutWriteLock, timeout, lockIds); } + } - foreach (var lockId in lockIds) + /// + /// Handles acquiring a lock, this should only be called from the outermost scope. + /// + /// Instance ID of the scope requesting the lock. + /// Reference to the applicable locks dictionary (ReadLocks or WriteLocks). + /// Reference to the applicable locks hashset (_readLocks or _writeLocks). + /// Delegate used to request the lock from the database without a timeout. + /// Delegate used to request the lock from the database with a timeout. + /// Optional timeout parameter to specify a timeout. + /// Lock identifiers to lock on. + private void LockInner(Guid instanceId, ref Dictionary> locks, ref HashSet locksSet, + Action obtainLock, Action obtainLockTimeout, TimeSpan? timeout = null, + params int[] lockIds) + { + lock (_dictionaryLocker) { - lock (_dictionaryLocker) + locksSet ??= new HashSet(); + foreach (var lockId in lockIds) { - // Only acquire lock if we haven't yet (WriteLocks not containing the key) - if (!WriteLocks.ContainsKey(lockId)) + // Only acquire the lock if we haven't done so yet. + if (!locksSet.Contains(lockId)) { - if (timeout is null) + IncrementLock(lockId, instanceId, ref locks); + locksSet.Add(lockId); + try { - ObtainWriteLock(lockId); + if (timeout is null) + { + // We just want an ordinary lock. + obtainLock(lockId); + } + else + { + // We want a lock with a custom timeout + obtainLockTimeout(lockId, timeout.Value); + } } - else + catch { - ObtainTimeoutWriteLock(lockId, timeout.Value); + // Something went wrong and we didn't get the lock + // Since we at this point have determined that we haven't got any lock with an ID of LockID, it's safe to completely remove it instead of decrementing. + locks[instanceId].Remove(lockId); + // It needs to be removed from the HashSet as well, because that's how we determine to acquire a lock. + locksSet.Remove(lockId); + throw; } - // Add the lockId as a key to the dict. - WriteLocks[lockId] = 0; } - - // Increment count of the lock by 1. - WriteLocks[lockId] += 1; + else + { + // We already have a lock, but need to update the dictionary for debugging purposes. + IncrementLock(lockId, instanceId, ref locks); + } } } } @@ -735,10 +718,10 @@ private void ObtainReadLock(int lockId) /// /// Lock object identifier to lock. /// TimeSpan specifying the timout period. - private void ObtainTimoutReadLock(int lockId, TimeSpan timeout) + private void ObtainTimeoutReadLock(int lockId, TimeSpan timeout) { var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; - if (syntax2 == null) + if (syntax2 is null) { throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); } @@ -763,7 +746,7 @@ private void ObtainWriteLock(int lockId) private void ObtainTimeoutWriteLock(int lockId, TimeSpan timeout) { var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; - if (syntax2 == null) + if (syntax2 is null) { throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); } diff --git a/src/Umbraco.Core/Scoping/ScopeProvider.cs b/src/Umbraco.Core/Scoping/ScopeProvider.cs index bf4e27bdb6c2..a1cc1281810d 100644 --- a/src/Umbraco.Core/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/ScopeProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Data; using System.Runtime.Remoting.Messaging; using System.Web; @@ -240,6 +241,9 @@ public ScopeContext AmbientContext var value = GetHttpContextObject(ContextItemKey, false); return value ?? GetCallContextObject(ContextItemKey); } + + [Obsolete("This setter is not used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] set { // clear both diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index e5363d0e2b11..d8e99663eac3 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1415,7 +1415,7 @@ private void PerformScheduledPublishingExpiration(DateTime date, List(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); results.Add(result); } @@ -2201,7 +2201,7 @@ public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool while (page * pageSize < total) { var descendants = GetPagedDescendants(content.Id, page++, pageSize, out total); - foreach (var descendant in descendants) + foreach (var descendant in descendants.OrderBy(x => x.Level).ThenBy(y => y.SortOrder)) { // if parent has not been copied, skip, else gets its copy id if (idmap.TryGetValue(descendant.ParentId, out parentId) == false) continue; @@ -2420,7 +2420,7 @@ public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportO if (report.FixedIssues.Count > 0) { //The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref - var root = new Content("root", -1, new ContentType(-1)) {Id = -1, Key = Guid.Empty}; + var root = new Content("root", -1, new ContentType(-1)) { Id = -1, Key = Guid.Empty }; scope.Events.Dispatch(TreeChanged, this, new TreeChange.EventArgs(new TreeChange(root, TreeChangeTypes.RefreshAll))); } @@ -3169,7 +3169,7 @@ public OperationResult Rollback(int id, int versionId, string culture = "*", int if (rollbackSaveResult.Success == false) { //Log the error/warning - Logger.Error("User '{UserId}' was unable to rollback content '{ContentId}' to version '{VersionId}'", userId, id, versionId); + Logger.Error("User '{UserId}' was unable to rollback content '{ContentId}' to version '{VersionId}'", userId, id, versionId); } else { @@ -3178,7 +3178,7 @@ public OperationResult Rollback(int id, int versionId, string culture = "*", int scope.Events.Dispatch(RolledBack, this, rollbackEventArgs); //Logging & Audit message - Logger.Info("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'", userId, id, versionId); + Logger.Info("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'", userId, id, versionId); Audit(AuditType.RollBack, userId, id, $"Content '{content.Name}' was rolled back to version '{versionId}'"); } diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 80ef81f36d04..0e16c2c85296 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -22,7 +22,7 @@ namespace Umbraco.Core /// public static class StringExtensions { - + private const char DefaultEscapedStringEscapeChar = '\\'; private static readonly char[] ToCSharpHexDigitLower = "0123456789abcdef".ToCharArray(); private static readonly char[] ToCSharpEscapeChars; @@ -1490,5 +1490,44 @@ private static void SwapBytes(byte[] guid, int left, int right) /// public static string NullOrWhiteSpaceAsNull(this string text) => string.IsNullOrWhiteSpace(text) ? null : text; + + /// + /// Splits a string with an escape character that allows for the split character to exist in a string + /// + /// The string to split + /// The character to split on + /// The character which can be used to escape the character to split on + /// The string split into substrings delimited by the split character + public static IEnumerable EscapedSplit(this string value, char splitChar, char escapeChar = DefaultEscapedStringEscapeChar) + { + if (value == null) yield break; + + var sb = new StringBuilder(value.Length); + var escaped = false; + + foreach (var chr in value.ToCharArray()) + { + if (escaped) + { + escaped = false; + sb.Append(chr); + } + else if (chr == splitChar) + { + yield return sb.ToString(); + sb.Clear(); + } + else if (chr == escapeChar) + { + escaped = true; + } + else + { + sb.Append(chr); + } + } + + yield return sb.ToString(); + } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index f0ba7f66d8b5..116088130428 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -133,6 +133,8 @@ + + @@ -270,7 +272,6 @@ - diff --git a/src/Umbraco.Examine/UmbracoExamineIndex.cs b/src/Umbraco.Examine/UmbracoExamineIndex.cs index cc97178e5c85..511d78db92d2 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndex.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndex.cs @@ -24,8 +24,8 @@ public abstract class UmbracoExamineIndex : LuceneIndex, IUmbracoIndex, IIndexDi // note // wrapping all operations that end up calling base.SafelyProcessQueueItems in a safe call // context because they will fork a thread/task/whatever which should *not* capture our - // call context (and the database it can contain)! ideally we should be able to override - // SafelyProcessQueueItems but that's not possible in the current version of Examine. + // call context (and the database it can contain)! + // TODO: FIX Examine to not flow the ExecutionContext so callers don't need to worry about this! /// /// Used to store the path of a content object @@ -99,6 +99,9 @@ protected override void PerformDeleteFromIndex(IEnumerable itemIds, Acti { if (CanInitialize()) { + // Use SafeCallContext to prevent the current CallContext flow to child + // tasks executed in the base class so we don't leak Scopes. + // TODO: See notes at the top of this class using (new SafeCallContext()) { base.PerformDeleteFromIndex(itemIds, onComplete); @@ -106,6 +109,20 @@ protected override void PerformDeleteFromIndex(IEnumerable itemIds, Acti } } + protected override void PerformIndexItems(IEnumerable values, Action onComplete) + { + if (CanInitialize()) + { + // Use SafeCallContext to prevent the current CallContext flow to child + // tasks executed in the base class so we don't leak Scopes. + // TODO: See notes at the top of this class + using (new SafeCallContext()) + { + base.PerformIndexItems(values, onComplete); + } + } + } + /// /// Returns true if the Umbraco application is in a state that we can initialize the examine indexes /// diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs index 45c4de5d2a29..01010cca660f 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Reflection; using Umbraco.Core; using Umbraco.Core.Logging; @@ -20,14 +21,11 @@ public sealed class ModelsBuilderComposer : ICoreComposer { public void Compose(Composition composition) { - var isLegacyModelsBuilderInstalled = IsLegacyModelsBuilderInstalled(); - - composition.Configs.Add(() => new ModelsBuilderConfig()); - if (isLegacyModelsBuilderInstalled) + if (IsExternalModelsBuilderInstalled() == true) { - ComposeForLegacyModelsBuilder(composition); + ComposeForExternalModelsBuilder(composition); return; } @@ -45,22 +43,35 @@ public void Compose(Composition composition) ComposeForDefaultModelsFactory(composition); } - private static bool IsLegacyModelsBuilderInstalled() + private static bool IsExternalModelsBuilderInstalled() { - Assembly legacyMbAssembly = null; + var assemblyNames = new[] + { + "Umbraco.ModelsBuider", + "ModelsBuilder.Umbraco" + }; + try { - legacyMbAssembly = Assembly.Load("Umbraco.ModelsBuilder"); + foreach (var name in assemblyNames) + { + var assembly = Assembly.Load(name); + + if (assembly != null) + { + return true; + } + } } - catch (System.Exception) + catch (Exception) { //swallow exception, DLL must not be there } - return legacyMbAssembly != null; + return false; } - private void ComposeForLegacyModelsBuilder(Composition composition) + private void ComposeForExternalModelsBuilder(Composition composition) { composition.Logger.Info("ModelsBuilder.Embedded is disabled, the external ModelsBuilder was detected."); composition.Components().Append(); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts index 1a40e8451f86..0cec374c5db3 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts @@ -1,5 +1,13 @@ /// -import { DocumentTypeBuilder, ContentBuilder, AliasHelper } from 'umbraco-cypress-testhelpers'; +import { + DocumentTypeBuilder, + ContentBuilder, + AliasHelper, + GridDataTypeBuilder, + PartialViewMacroBuilder, + MacroBuilder +} from 'umbraco-cypress-testhelpers'; + context('Content', () => { beforeEach(() => { @@ -14,6 +22,23 @@ context('Content', () => { cy.get('.umb-tree-item__inner').should('exist', {timeout: 10000}); } + function createSimpleMacro(name){ + const insertMacro = new PartialViewMacroBuilder() + .withName(name) + .withContent(`@inherits Umbraco.Web.Macros.PartialViewMacroPage +

Acceptance test

`) + .build(); + + const macroWithPartial = new MacroBuilder() + .withName(name) + .withPartialViewMacro(insertMacro) + .withRenderInEditor() + .withUseInEditor() + .build(); + + cy.saveMacroWithPartial(macroWithPartial); + } + it('Copy content', () => { const rootDocTypeName = "Test document type"; const childDocTypeName = "Child test document type"; @@ -596,4 +621,181 @@ context('Content', () => { cy.umbracoEnsureTemplateNameNotExists(pickerDocTypeName); cy.umbracoEnsureDocumentTypeNameNotExists(pickedDocTypeName); }); + + it('Content with macro in RTE', () => { + const viewMacroName = 'Content with macro in RTE'; + const partialFileName = viewMacroName + '.cshtml'; + + cy.umbracoEnsureMacroNameNotExists(viewMacroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(partialFileName); + cy.umbracoEnsureDocumentTypeNameNotExists(viewMacroName); + cy.umbracoEnsureTemplateNameNotExists(viewMacroName); + cy.deleteAllContent(); + + // First thing first we got to create the macro we will be inserting + createSimpleMacro(viewMacroName); + + // Now we need to create a document type with a rich text editor where we can insert the macro + // The document type must have a template as well in order to ensure that the content is displayed correctly + const alias = AliasHelper.toAlias(viewMacroName); + const docType = new DocumentTypeBuilder() + .withName(viewMacroName) + .withAlias(alias) + .withAllowAsRoot(true) + .withDefaultTemplate(alias) + .addGroup() + .addRichTextProperty() + .withAlias('text') + .done() + .done() + .build(); + + cy.saveDocumentType(docType).then((generatedDocType) => { + // Might as wel initally create the content here, the less GUI work during the test the better + const contentNode = new ContentBuilder() + .withContentTypeAlias(generatedDocType["alias"]) + .withAction('saveNew') + .addVariant() + .withName(viewMacroName) + .withSave(true) + .done() + .build(); + + cy.saveContent(contentNode); + }); + + // Edit the macro template in order to have something to verify on when rendered. + cy.editTemplate(viewMacroName, `@inherits Umbraco.Web.Mvc.UmbracoViewPage +@using ContentModels = Umbraco.Web.PublishedModels; +@{ + Layout = null; +} +@{ + if (Model.HasValue("text")){ + @(Model.Value("text")) + } +} `); + + // Enter content + refreshContentTree(); + cy.umbracoTreeItem("content", [viewMacroName]).click(); + + // Insert macro + cy.get('#mceu_13-button').click(); + cy.get('.umb-card-grid-item').contains(viewMacroName).click(); + + // Save and publish + cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); + cy.umbracoSuccessNotification().should('be.visible'); + + // Ensure that the view gets rendered correctly + const expected = `

Acceptance test

 

`; + cy.umbracoVerifyRenderedViewContent('/', expected, true).should('be.true'); + + // Cleanup + cy.umbracoEnsureMacroNameNotExists(viewMacroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(partialFileName); + cy.umbracoEnsureDocumentTypeNameNotExists(viewMacroName); + cy.umbracoEnsureTemplateNameNotExists(viewMacroName); + }); + + it('Content with macro in grid', () => { + const name = 'Content with macro in grid'; + const macroName = 'Grid macro'; + const macroFileName = macroName + '.cshtml'; + + cy.umbracoEnsureDataTypeNameNotExists(name); + cy.umbracoEnsureDocumentTypeNameNotExists(name); + cy.umbracoEnsureTemplateNameNotExists(name); + cy.umbracoEnsureMacroNameNotExists(macroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(macroFileName); + cy.deleteAllContent(); + + createSimpleMacro(macroName); + + const grid = new GridDataTypeBuilder() + .withName(name) + .withDefaultGrid() + .build(); + + const alias = AliasHelper.toAlias(name); + // Save grid and get the ID + cy.saveDataType(grid).then((dataType) => { + // Create a document type using the data type + const docType = new DocumentTypeBuilder() + .withName(name) + .withAlias(alias) + .withAllowAsRoot(true) + .withDefaultTemplate(alias) + .addGroup() + .addCustomProperty(dataType['id']) + .withAlias('grid') + .done() + .done() + .build(); + + cy.saveDocumentType(docType).then((generatedDocType) => { + const contentNode = new ContentBuilder() + .withContentTypeAlias(generatedDocType["alias"]) + .addVariant() + .withName(name) + .withSave(true) + .done() + .build(); + + cy.saveContent(contentNode); + }); + }); + + // Edit the template to allow us to verify the rendered view + cy.editTemplate(name, `@inherits Umbraco.Web.Mvc.UmbracoViewPage +@using ContentModels = Umbraco.Web.PublishedModels; +@{ + Layout = null; +} +@Html.GetGridHtml(Model, "grid")`); + + // Act + // Enter content + refreshContentTree(); + cy.umbracoTreeItem("content", [name]).click(); + // Click add + cy.get(':nth-child(2) > .preview-row > .preview-col > .preview-cell').click(); // Choose 1 column layout. + cy.get('.umb-column > .templates-preview > :nth-child(2) > small').click(); // Choose headline + cy.get('.umb-cell-placeholder').click(); + // Click macro + cy.get(':nth-child(4) > .umb-card-grid-item > :nth-child(1)').click(); + // Select the macro + cy.get('.umb-card-grid-item').contains(macroName).click(); + + // Save and publish + cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); + cy.umbracoSuccessNotification().should('be.visible'); + + const expected = ` +
+
+
+
+
+
+
+

Acceptance test

+
+
+
+
+
+
+
` + + cy.umbracoVerifyRenderedViewContent('/', expected, true).should('be.true'); + + // Clean + cy.umbracoEnsureDataTypeNameNotExists(name); + cy.umbracoEnsureDocumentTypeNameNotExists(name); + cy.umbracoEnsureTemplateNameNotExists(name); + cy.umbracoEnsureMacroNameNotExists(macroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(macroFileName); + }); }); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts index 49bcf94943e5..336e5793d909 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts @@ -6,7 +6,10 @@ context('Languages', () => { }); it('Add language', () => { - const name = "Kyrgyz (Kyrgyzstan)"; // Must be an option in the select box + // For some reason the languages to chose fom seems to be translated differently than normal, as an example: + // My system is set to EN (US), but most languages are translated into Danish for some reason + // Aghem seems untranslated though? + const name = "Aghem"; // Must be an option in the select box cy.umbracoEnsureLanguageNameNotExists(name); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts index c586384af714..65d03e5a7841 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts @@ -23,14 +23,18 @@ context('Templates', () => { cy.umbracoEnsureTemplateNameNotExists(name); createTemplate(); + // We have to wait for the ace editor to load, because when the editor is loading it will "steal" the focus briefly, + // which causes the save event to fire if we've added something to the header field, causing errors. + cy.wait(500); + //Type name cy.umbracoEditorHeaderName(name); // Save // We must drop focus for the auto save event to occur. cy.get('.btn-success').focus(); // And then wait for the auto save event to finish by finding the page in the tree view. - // This is a bit of a roundabout way to find items in a treev view since we dont use umbracoTreeItem - // but we must be able to wait for the save evnent to finish, and we can't do that with umbracoTreeItem + // This is a bit of a roundabout way to find items in a tree view since we dont use umbracoTreeItem + // but we must be able to wait for the save event to finish, and we can't do that with umbracoTreeItem cy.get('[data-element="tree-item-templates"] > :nth-child(2) > .umb-animated > .umb-tree-item__inner > .umb-tree-item__label') .contains(name).should('be.visible', { timeout: 10000 }); // Now that the auto save event has finished we can save diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts index 9bc1fff488f4..d3950d7d1928 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts @@ -49,7 +49,7 @@ function resetTourData() { { "alias": "umbIntroIntroduction", "completed": false, - "disabled": false + "disabled": true }; cy.getCookie('UMB-XSRF-TOKEN', { log: false }).then((token) => { diff --git a/src/Umbraco.Tests.AcceptanceTest/package.json b/src/Umbraco.Tests.AcceptanceTest/package.json index 378fe719fcd4..caf75638e611 100644 --- a/src/Umbraco.Tests.AcceptanceTest/package.json +++ b/src/Umbraco.Tests.AcceptanceTest/package.json @@ -7,10 +7,10 @@ }, "devDependencies": { "cross-env": "^7.0.2", - "cypress": "^6.0.1", + "cypress": "^6.8.0", "ncp": "^2.0.0", "prompt": "^1.0.0", - "umbraco-cypress-testhelpers": "^1.0.0-beta-52" + "umbraco-cypress-testhelpers": "^1.0.0-beta-53" }, "dependencies": { "typescript": "^3.9.2" diff --git a/src/Umbraco.Tests/Mapping/MappingTests.cs b/src/Umbraco.Tests/Mapping/MappingTests.cs index e6a382692c69..35f64cac62db 100644 --- a/src/Umbraco.Tests/Mapping/MappingTests.cs +++ b/src/Umbraco.Tests/Mapping/MappingTests.cs @@ -1,17 +1,40 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Threading; +using Moq; using NUnit.Framework; +using Umbraco.Core.Events; using Umbraco.Core.Mapping; using Umbraco.Core.Models; +using Umbraco.Core.Scoping; using Umbraco.Web.Models.ContentEditing; +using PropertyCollection = Umbraco.Core.Models.PropertyCollection; namespace Umbraco.Tests.Mapping { [TestFixture] public class MappingTests { + private IScopeProvider _scopeProvider; + + [SetUp] + public void MockScopeProvider() + { + var scopeMock = new Mock(); + scopeMock.Setup(x => x.CreateScope( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Mock.Of); + + _scopeProvider = scopeMock.Object; + } + [Test] public void SimpleMap() { @@ -19,7 +42,7 @@ public void SimpleMap() { new MapperDefinition1(), }); - var mapper = new UmbracoMapper(definitions); + var mapper = new UmbracoMapper(definitions, _scopeProvider); var thing1 = new Thing1 { Value = "value" }; var thing2 = mapper.Map(thing1); @@ -44,7 +67,7 @@ public void EnumerableMap() { new MapperDefinition1(), }); - var mapper = new UmbracoMapper(definitions); + var mapper = new UmbracoMapper(definitions, _scopeProvider); var thing1A = new Thing1 { Value = "valueA" }; var thing1B = new Thing1 { Value = "valueB" }; @@ -78,7 +101,7 @@ public void InheritedMap() { new MapperDefinition1(), }); - var mapper = new UmbracoMapper(definitions); + var mapper = new UmbracoMapper(definitions, _scopeProvider); var thing3 = new Thing3 { Value = "value" }; var thing2 = mapper.Map(thing3); @@ -103,7 +126,7 @@ public void CollectionsMap() { new MapperDefinition2(), }); - var mapper = new UmbracoMapper(definitions); + var mapper = new UmbracoMapper(definitions, _scopeProvider); // can map a PropertyCollection var source = new PropertyCollection(); @@ -119,7 +142,7 @@ public void ConcurrentMap() new MapperDefinition1(), new MapperDefinition3(), }); - var mapper = new UmbracoMapper(definitions); + var mapper = new UmbracoMapper(definitions, _scopeProvider); // the mapper currently has a map from Thing1 to Thing2 // because Thing3 inherits from Thing1, it will map a Thing3 instance, @@ -179,7 +202,7 @@ public void EnumMap() { new MapperDefinition4(), }); - var mapper = new UmbracoMapper(definitions); + var mapper = new UmbracoMapper(definitions, _scopeProvider); var thing5 = new Thing5() { @@ -203,7 +226,7 @@ public void NullPropertyMap() { new MapperDefinition5(), }); - var mapper = new UmbracoMapper(definitions); + var mapper = new UmbracoMapper(definitions, _scopeProvider); var thing7 = new Thing7(); diff --git a/src/Umbraco.Tests/Scoping/ScopeTests.cs b/src/Umbraco.Tests/Scoping/ScopeTests.cs index 6c5e9a74b554..7d8984baad73 100644 --- a/src/Umbraco.Tests/Scoping/ScopeTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeTests.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Runtime.Remoting.Messaging; using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Persistence; @@ -24,6 +25,119 @@ public override void SetUp() Assert.IsNull(ScopeProvider.AmbientScope); // gone } + [Test] + public void GivenUncompletedScopeOnChildThread_WhenTheParentCompletes_TheTransactionIsRolledBack() + { + ScopeProvider scopeProvider = ScopeProvider; + + Assert.IsNull(ScopeProvider.AmbientScope); + IScope mainScope = scopeProvider.CreateScope(); + + var t = Task.Run(() => + { + IScope nested = scopeProvider.CreateScope(); + Thread.Sleep(2000); + nested.Dispose(); + }); + + Thread.Sleep(1000); // mimic some long running operation that is shorter than the other thread + mainScope.Complete(); + Assert.Throws(() => mainScope.Dispose()); + + Task.WaitAll(t); + } + + [Test] + public void GivenNonDisposedChildScope_WhenTheParentDisposes_ThenInvalidOperationExceptionThrows() + { + // this all runs in the same execution context so the AmbientScope reference isn't a copy + + ScopeProvider scopeProvider = ScopeProvider; + + Assert.IsNull(ScopeProvider.AmbientScope); + IScope mainScope = scopeProvider.CreateScope(); + + IScope nested = scopeProvider.CreateScope(); // not disposing + + InvalidOperationException ex = Assert.Throws(() => mainScope.Dispose()); + Console.WriteLine(ex); + } + + [Test] + public void GivenChildThread_WhenParentDisposedBeforeChild_ParentScopeThrows() + { + // The ambient context is NOT thread safe, even though it has locks, etc... + // This all just goes to show that concurrent threads with scopes is a no-go. + var childWait = new ManualResetEventSlim(false); + var parentWait = new ManualResetEventSlim(false); + + ScopeProvider scopeProvider = ScopeProvider; + + Assert.IsNull(ScopeProvider.AmbientScope); + IScope mainScope = scopeProvider.CreateScope(); + + var t = Task.Run(() => + { + Console.WriteLine("Child Task start: " + scopeProvider.AmbientScope.InstanceId); + // This will evict the parent from the ScopeProvider.StaticCallContextObjects + // and replace it with the child + IScope nested = scopeProvider.CreateScope(); + childWait.Set(); + Console.WriteLine("Child Task scope created: " + scopeProvider.AmbientScope.InstanceId); + parentWait.Wait(); // wait for the parent thread + Console.WriteLine("Child Task before dispose: " + scopeProvider.AmbientScope.InstanceId); + // This will evict the child from the ScopeProvider.StaticCallContextObjects + // and replace it with the parent + nested.Dispose(); + Console.WriteLine("Child Task after dispose: " + scopeProvider.AmbientScope.InstanceId); + }); + + childWait.Wait(); // wait for the child to start and create the scope + // This is a confusing thing (this is not the case in netcore), this is NULL because the + // parent thread's scope ID was evicted from the ScopeProvider.StaticCallContextObjects + // so now the ambient context is null because the GUID in the CallContext doesn't match + // the GUID in the ScopeProvider.StaticCallContextObjects. + Assert.IsNull(scopeProvider.AmbientScope); + // now dispose the main without waiting for the child thread to join + // This will throw because at this stage a child scope has been created which means + // it is the Ambient (top) scope but here we're trying to dispose the non top scope. + Assert.Throws(() => mainScope.Dispose()); + parentWait.Set(); // tell child thread to proceed + Task.WaitAll(t); // wait for the child to dispose + mainScope.Dispose(); // now it's ok + Console.WriteLine("Parent Task disposed: " + scopeProvider.AmbientScope?.InstanceId); + } + + [Test] + public void GivenChildThread_WhenChildDisposedBeforeParent_OK() + { + ScopeProvider scopeProvider = ScopeProvider; + + Assert.IsNull(ScopeProvider.AmbientScope); + IScope mainScope = scopeProvider.CreateScope(); + + // Task.Run will flow the execution context unless ExecutionContext.SuppressFlow() is explicitly called. + // This is what occurs in normal async behavior since it is expected to await (and join) the main thread, + // but if Task.Run is used as a fire and forget thread without being done correctly then the Scope will + // flow to that thread. + var t = Task.Run(() => + { + Console.WriteLine("Child Task start: " + scopeProvider.AmbientScope.InstanceId); + IScope nested = scopeProvider.CreateScope(); + Console.WriteLine("Child Task before dispose: " + scopeProvider.AmbientScope.InstanceId); + nested.Dispose(); + Console.WriteLine("Child Task after disposed: " + scopeProvider.AmbientScope.InstanceId); + }); + + Console.WriteLine("Parent Task waiting: " + scopeProvider.AmbientScope?.InstanceId); + Task.WaitAll(t); + Console.WriteLine("Parent Task disposing: " + scopeProvider.AmbientScope.InstanceId); + mainScope.Dispose(); + Console.WriteLine("Parent Task disposed: " + scopeProvider.AmbientScope?.InstanceId); + + Assert.Pass(); + } + [Test] public void SimpleCreateScope() { diff --git a/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs b/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs index 32bd7e2afe5a..038376f71ca1 100644 --- a/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Moq; using NPoco; using NUnit.Framework; @@ -9,6 +10,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Scoping; +using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests.Scoping { @@ -72,6 +74,30 @@ public void WriteLock_Acquired_Only_Once_Per_Key() syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.Languages), Times.Once); } + [Test] + public void WriteLock_Acquired_Only_Once_When_InnerScope_Disposed() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var outerScope = scopeProvider.CreateScope()) + { + outerScope.WriteLock(Constants.Locks.Languages); + + using (var innerScope = scopeProvider.CreateScope()) + { + innerScope.WriteLock(Constants.Locks.Languages); + innerScope.WriteLock(Constants.Locks.ContentTree); + innerScope.Complete(); + } + + outerScope.WriteLock(Constants.Locks.ContentTree); + outerScope.Complete(); + } + + syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.Languages), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.ContentTree), Times.Once); + } + [Test] public void WriteLock_With_Timeout_Acquired_Only_Once_Per_Key(){ var scopeProvider = GetScopeProvider(out var syntaxProviderMock); @@ -176,31 +202,58 @@ public void ReadLock_With_Timeout_Acquired_Only_Once_Per_Key() syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), timeOut, Constants.Locks.Languages), Times.Once); } + [Test] + public void ReadLock_Acquired_Only_Once_When_InnerScope_Disposed() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var outerScope = scopeProvider.CreateScope()) + { + outerScope.ReadLock(Constants.Locks.Languages); + + using (var innerScope = scopeProvider.CreateScope()) + { + innerScope.ReadLock(Constants.Locks.Languages); + innerScope.ReadLock(Constants.Locks.ContentTree); + innerScope.Complete(); + } + + outerScope.ReadLock(Constants.Locks.ContentTree); + outerScope.Complete(); + } + + syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.Languages), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.ContentTree), Times.Once); + } + [Test] public void WriteLocks_Count_correctly_If_Lock_Requested_Twice_In_Scope() { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + Guid innerscopeId; using (var outerscope = scopeProvider.CreateScope()) { var realOuterScope = (Scope) outerscope; outerscope.WriteLock(Constants.Locks.ContentTree); outerscope.WriteLock(Constants.Locks.ContentTree); - Assert.AreEqual(2, realOuterScope.WriteLocks[Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.WriteLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); using (var innerScope = scopeProvider.CreateScope()) { + innerscopeId = innerScope.InstanceId; innerScope.WriteLock(Constants.Locks.ContentTree); innerScope.WriteLock(Constants.Locks.ContentTree); - Assert.AreEqual(4, realOuterScope.WriteLocks[Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.WriteLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.WriteLocks[innerscopeId][Constants.Locks.ContentTree]); innerScope.WriteLock(Constants.Locks.Languages); innerScope.WriteLock(Constants.Locks.Languages); - Assert.AreEqual(2, realOuterScope.WriteLocks[Constants.Locks.Languages]); + Assert.AreEqual(2, realOuterScope.WriteLocks[innerScope.InstanceId][Constants.Locks.Languages]); innerScope.Complete(); } - Assert.AreEqual(0, realOuterScope.WriteLocks[Constants.Locks.Languages]); - Assert.AreEqual(2, realOuterScope.WriteLocks[Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.WriteLocks[realOuterScope.InstanceId][Constants.Locks.ContentTree]); + Assert.IsFalse(realOuterScope.WriteLocks.ContainsKey(innerscopeId)); outerscope.Complete(); } } @@ -209,27 +262,32 @@ public void WriteLocks_Count_correctly_If_Lock_Requested_Twice_In_Scope() public void ReadLocks_Count_correctly_If_Lock_Requested_Twice_In_Scope() { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + Guid innerscopeId; using (var outerscope = scopeProvider.CreateScope()) { var realOuterScope = (Scope) outerscope; outerscope.ReadLock(Constants.Locks.ContentTree); outerscope.ReadLock(Constants.Locks.ContentTree); - Assert.AreEqual(2, realOuterScope.ReadLocks[Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.ReadLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); using (var innerScope = scopeProvider.CreateScope()) { + innerscopeId = innerScope.InstanceId; innerScope.ReadLock(Constants.Locks.ContentTree); innerScope.ReadLock(Constants.Locks.ContentTree); - Assert.AreEqual(4, realOuterScope.ReadLocks[Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.ReadLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.ReadLocks[innerScope.InstanceId][Constants.Locks.ContentTree]); innerScope.ReadLock(Constants.Locks.Languages); innerScope.ReadLock(Constants.Locks.Languages); - Assert.AreEqual(2, realOuterScope.ReadLocks[Constants.Locks.Languages]); + Assert.AreEqual(2, realOuterScope.ReadLocks[innerScope.InstanceId][Constants.Locks.Languages]); innerScope.Complete(); } - Assert.AreEqual(0, realOuterScope.ReadLocks[Constants.Locks.Languages]); - Assert.AreEqual(2, realOuterScope.ReadLocks[Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.ReadLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); + Assert.IsFalse(realOuterScope.ReadLocks.ContainsKey(innerscopeId)); + + outerscope.Complete(); } } @@ -238,51 +296,61 @@ public void ReadLocks_Count_correctly_If_Lock_Requested_Twice_In_Scope() public void Nested_Scopes_WriteLocks_Count_Correctly() { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + Guid innerScope1Id, innerScope2Id; - using (var outerScope = scopeProvider.CreateScope()) + using (var parentScope = scopeProvider.CreateScope()) { - var parentScope = (Scope) outerScope; - outerScope.WriteLock(Constants.Locks.ContentTree); - outerScope.WriteLock(Constants.Locks.ContentTypes); + var realParentScope = (Scope) parentScope; + parentScope.WriteLock(Constants.Locks.ContentTree); + parentScope.WriteLock(Constants.Locks.ContentTypes); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); using (var innerScope1 = scopeProvider.CreateScope()) { + innerScope1Id = innerScope1.InstanceId; innerScope1.WriteLock(Constants.Locks.ContentTree); innerScope1.WriteLock(Constants.Locks.ContentTypes); innerScope1.WriteLock(Constants.Locks.Languages); - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope1 after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); using (var innerScope2 = scopeProvider.CreateScope()) { + innerScope2Id = innerScope2.InstanceId; innerScope2.WriteLock(Constants.Locks.ContentTree); innerScope2.WriteLock(Constants.Locks.MediaTypes); - Assert.AreEqual(3, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope2 after locks acquired: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.Languages], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope2.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope2.InstanceId][Constants.Locks.MediaTypes], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); innerScope2.Complete(); } - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.MediaTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after innserScope2 disposed: {nameof(Constants.Locks.Languages)}"); + Assert.IsFalse(realParentScope.WriteLocks.ContainsKey(innerScope2Id)); innerScope1.Complete(); } - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.Languages], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.MediaTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.IsFalse(realParentScope.WriteLocks.ContainsKey(innerScope2Id)); + Assert.IsFalse(realParentScope.WriteLocks.ContainsKey(innerScope1Id)); - outerScope.Complete(); + parentScope.Complete(); } } @@ -290,48 +358,166 @@ public void Nested_Scopes_WriteLocks_Count_Correctly() public void Nested_Scopes_ReadLocks_Count_Correctly() { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + Guid innerScope1Id, innerScope2Id; - using (var outerScope = scopeProvider.CreateScope()) + using (var parentScope = scopeProvider.CreateScope()) { - var parentScope = (Scope) outerScope; - outerScope.ReadLock(Constants.Locks.ContentTree); - outerScope.ReadLock(Constants.Locks.ContentTypes); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + var realParentScope = (Scope) parentScope; + parentScope.ReadLock(Constants.Locks.ContentTree); + parentScope.ReadLock(Constants.Locks.ContentTypes); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); using (var innserScope1 = scopeProvider.CreateScope()) { + innerScope1Id = innserScope1.InstanceId; innserScope1.ReadLock(Constants.Locks.ContentTree); innserScope1.ReadLock(Constants.Locks.ContentTypes); innserScope1.ReadLock(Constants.Locks.Languages); - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope1 after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); using (var innerScope2 = scopeProvider.CreateScope()) { + innerScope2Id = innerScope2.InstanceId; innerScope2.ReadLock(Constants.Locks.ContentTree); innerScope2.ReadLock(Constants.Locks.MediaTypes); - Assert.AreEqual(3, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope2 after locks acquired: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.Languages], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innerScope2.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innerScope2.InstanceId][Constants.Locks.MediaTypes], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); innerScope2.Complete(); } - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.MediaTypes)}"); + + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after innerScope2 disposed: {nameof(Constants.Locks.Languages)}"); + Assert.IsFalse(realParentScope.ReadLocks.ContainsKey(innerScope2Id)); innserScope1.Complete(); } - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.Languages], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.MediaTypes)}"); - outerScope.Complete(); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after innerScope1 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after innerScope1 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.IsFalse(realParentScope.ReadLocks.ContainsKey(innerScope2Id)); + Assert.IsFalse(realParentScope.ReadLocks.ContainsKey(innerScope1Id)); + + parentScope.Complete(); + } + } + + [Test] + public void WriteLock_Doesnt_Increment_On_Error() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + syntaxProviderMock.Setup(x => x.WriteLock(It.IsAny(), It.IsAny())).Throws(new Exception("Boom")); + + using (var scope = scopeProvider.CreateScope()) + { + var realScope = (Scope) scope; + + Assert.Throws(() => scope.WriteLock(Constants.Locks.Languages)); + Assert.IsFalse(realScope.WriteLocks[scope.InstanceId].ContainsKey(Constants.Locks.Languages)); + scope.Complete(); + } + } + + [Test] + public void ReadLock_Doesnt_Increment_On_Error() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + syntaxProviderMock.Setup(x => x.ReadLock(It.IsAny(), It.IsAny())).Throws(new Exception("Boom")); + + using (var scope = scopeProvider.CreateScope()) + { + var realScope = (Scope) scope; + + Assert.Throws(() => scope.ReadLock(Constants.Locks.Languages)); + Assert.IsFalse(realScope.ReadLocks[scope.InstanceId].ContainsKey(Constants.Locks.Languages)); + scope.Complete(); + } + } + + [Test] + public void Scope_Throws_If_ReadLocks_Not_Cleared() + { + var scopeprovider = GetScopeProvider(out var syntaxProviderMock); + var scope = (Scope) scopeprovider.CreateScope(); + + try + { + // Request a lock to create the ReadLocks dict. + scope.ReadLock(Constants.Locks.Domains); + + var readDict = new Dictionary(); + readDict[Constants.Locks.Languages] = 1; + scope.ReadLocks[Guid.NewGuid()] = readDict; + + Assert.Throws(() => scope.Dispose()); + } + finally + { + // We have to clear so we can properly dispose the scope, otherwise it'll mess with other tests. + scope.ReadLocks?.Clear(); + scope.Dispose(); + } + } + + [Test] + public void Scope_Throws_If_WriteLocks_Not_Cleared() + { + var scopeprovider = GetScopeProvider(out var syntaxProviderMock); + var scope = (Scope) scopeprovider.CreateScope(); + + try + { + // Request a lock to create the WriteLocks dict. + scope.WriteLock(Constants.Locks.Domains); + + var writeDict = new Dictionary(); + writeDict[Constants.Locks.Languages] = 1; + scope.WriteLocks[Guid.NewGuid()] = writeDict; + + Assert.Throws(() => scope.Dispose()); + } + finally + { + // We have to clear so we can properly dispose the scope, otherwise it'll mess with other tests. + scope.WriteLocks?.Clear(); + scope.Dispose(); + } + } + + [Test] + public void WriteLocks_Not_Created_Until_First_Lock() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var scope = scopeProvider.CreateScope()) + { + var realScope = (Scope) scope; + Assert.IsNull(realScope.WriteLocks); + } + } + + [Test] + public void ReadLocks_Not_Created_Until_First_Lock() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var scope = scopeProvider.CreateScope()) + { + var realScope = (Scope) scope; + Assert.IsNull(realScope.ReadLocks); } } } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 008c24fcbfa3..0faa4af316e6 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -2082,6 +2082,32 @@ public void Can_Copy_Content_With_Tags() Assert.AreEqual("world", copiedTags[1].Text); } + [Test] + public void Copy_Recursive_Preserves_Sort_Order() + { + // Arrange + var contentService = ServiceContext.ContentService; + var temp = contentService.GetById(NodeDto.NodeIdSeed + 2); + Assert.AreEqual("Home", temp.Name); + Assert.AreEqual(3, contentService.CountChildren(temp.Id)); + var reversedChildren = contentService.GetPagedChildren(temp.Id, 0, 10, out var total1).Reverse().ToArray(); + contentService.Sort(reversedChildren); + + // Act + var copy = contentService.Copy(temp, temp.ParentId, false, true, Constants.Security.SuperUserId); + var content = contentService.GetById(NodeDto.NodeIdSeed + 2); + + // Assert + Assert.That(copy, Is.Not.Null); + Assert.That(copy.Id, Is.Not.EqualTo(content.Id)); + Assert.AreNotSame(content, copy); + Assert.AreEqual(3, contentService.CountChildren(copy.Id)); + + var copiedChildren = contentService.GetPagedChildren(copy.Id, 0, 10, out var total2).OrderBy(c => c.SortOrder).ToArray(); + Assert.AreEqual(reversedChildren.First().Name, copiedChildren.First().Name); + Assert.AreEqual(reversedChildren.Last().Name, copiedChildren.Last().Name); + } + [Test] public void Can_Rollback_Version_On_Content() { diff --git a/src/Umbraco.Tests/Services/Importing/StandardMvc-Package.xml b/src/Umbraco.Tests/Services/Importing/StandardMvc-Package.xml index ee6f7cea4af4..daeb74cc7511 100644 --- a/src/Umbraco.Tests/Services/Importing/StandardMvc-Package.xml +++ b/src/Umbraco.Tests/Services/Importing/StandardMvc-Package.xml @@ -210,7 +210,7 @@ Google Maps - A map macro that you can use within Rich Text Areas Built by Creative Founds

Web ApplicationsCreative Founds design and build first class software solutions that deliver big results. We provide ASP.NET web and mobile applications, Umbraco development service & technical consultancy.

-

www.creativefounds.co.uk

]]> +

www.creativefounds.co.uk

]]>
Umbraco Development @@ -218,7 +218,7 @@ Google Maps - A map macro that you can use within Rich Text Areas Contact Us -

Contact Us on TwitterWe'd love to hear how this package has helped you and how it can be improved. Get in touch on the project website or via twitter

]]> +

Contact Us on TwitterWe'd love to hear how this package has helped you and how it can be improved. Get in touch on the project website or via twitter

]]>
Standard Website MVC, Company Address, Glasgow, Postcode
@@ -418,7 +418,7 @@ Google Maps - A map macro that you can use within Rich Text Areas Standard Website MVC

Well hello! This website package demonstrates all the standard functionality of Umbraco. It's a great starting point for starting point for further development or as a prototype.

Creative Founds

-

This package was developed by Chris Koiak & Creative Founds

]]> +

This package was developed by Chris Koiak & Creative Founds

]]> 1 diff --git a/src/Umbraco.Tests/Services/PerformanceTests.cs b/src/Umbraco.Tests/Services/PerformanceTests.cs index 9cf38e17891f..718f99ce2faa 100644 --- a/src/Umbraco.Tests/Services/PerformanceTests.cs +++ b/src/Umbraco.Tests/Services/PerformanceTests.cs @@ -297,11 +297,11 @@ private IEnumerable PrimeDbWithLotsOfContentXmlRecords(Guid customObjec Built by Creative Founds

Web ApplicationsCreative Founds design and build first class software solutions that deliver big results. We provide ASP.NET web and mobile applications, Umbraco development service & technical consultancy.

-

www.creativefounds.co.uk

]]>
+

www.creativefounds.co.uk

]]> Umbraco Development

UmbracoUmbraco the the leading ASP.NET open source CMS, under pinning over 150,000 websites. Our Certified Developers are experts in developing high performance and feature rich websites.

]]>
Contact Us -

Contact Us on TwitterWe'd love to hear how this package has helped you and how it can be improved. Get in touch on the project website or via twitter

]]>
+

Contact Us on TwitterWe'd love to hear how this package has helped you and how it can be improved. Get in touch on the project website or via twitter

]]>
Standard Website MVC, Company Address, Glasgow, Postcode
Copyright &copy; 2012 Your Company diff --git a/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs b/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs index 33ee2f737ac5..5c58b35b6d9e 100644 --- a/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs +++ b/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs @@ -104,7 +104,7 @@ public void Ensure_All_Threads_Execute_Successfully_Content_Service() var threads = new List(); var exceptions = new List(); - Debug.WriteLine("Starting..."); + Console.WriteLine("Starting..."); var done = TraceLocks(); @@ -114,12 +114,12 @@ public void Ensure_All_Threads_Execute_Successfully_Content_Service() { try { - Debug.WriteLine("[{0}] Running...", Thread.CurrentThread.ManagedThreadId); + Console.WriteLine("[{0}] Running...", Thread.CurrentThread.ManagedThreadId); var name1 = "test-" + Guid.NewGuid(); var content1 = contentService.Create(name1, -1, "umbTextpage"); - Debug.WriteLine("[{0}] Saving content #1.", Thread.CurrentThread.ManagedThreadId); + Console.WriteLine("[{0}] Saving content #1.", Thread.CurrentThread.ManagedThreadId); Save(contentService, content1); Thread.Sleep(100); //quick pause for maximum overlap! @@ -127,7 +127,7 @@ public void Ensure_All_Threads_Execute_Successfully_Content_Service() var name2 = "test-" + Guid.NewGuid(); var content2 = contentService.Create(name2, -1, "umbTextpage"); - Debug.WriteLine("[{0}] Saving content #2.", Thread.CurrentThread.ManagedThreadId); + Console.WriteLine("[{0}] Saving content #2.", Thread.CurrentThread.ManagedThreadId); Save(contentService, content2); } catch (Exception e) @@ -139,16 +139,16 @@ public void Ensure_All_Threads_Execute_Successfully_Content_Service() } // start all threads - Debug.WriteLine("Starting threads"); + Console.WriteLine("Starting threads"); threads.ForEach(x => x.Start()); // wait for all to complete - Debug.WriteLine("Joining threads"); + Console.WriteLine("Joining threads"); threads.ForEach(x => x.Join()); done.Set(); - Debug.WriteLine("Checking exceptions"); + Console.WriteLine("Checking exceptions"); if (exceptions.Count == 0) { //now look up all items, there should be 40! @@ -172,7 +172,7 @@ public void Ensure_All_Threads_Execute_Successfully_Media_Service() var threads = new List(); var exceptions = new List(); - Debug.WriteLine("Starting..."); + Console.WriteLine("Starting..."); var done = TraceLocks(); @@ -182,18 +182,18 @@ public void Ensure_All_Threads_Execute_Successfully_Media_Service() { try { - Debug.WriteLine("[{0}] Running...", Thread.CurrentThread.ManagedThreadId); + Console.WriteLine("[{0}] Running...", Thread.CurrentThread.ManagedThreadId); var name1 = "test-" + Guid.NewGuid(); var media1 = mediaService.CreateMedia(name1, -1, Constants.Conventions.MediaTypes.Folder); - Debug.WriteLine("[{0}] Saving media #1.", Thread.CurrentThread.ManagedThreadId); + Console.WriteLine("[{0}] Saving media #1.", Thread.CurrentThread.ManagedThreadId); Save(mediaService, media1); Thread.Sleep(100); //quick pause for maximum overlap! var name2 = "test-" + Guid.NewGuid(); var media2 = mediaService.CreateMedia(name2, -1, Constants.Conventions.MediaTypes.Folder); - Debug.WriteLine("[{0}] Saving media #2.", Thread.CurrentThread.ManagedThreadId); + Console.WriteLine("[{0}] Saving media #2.", Thread.CurrentThread.ManagedThreadId); Save(mediaService, media2); } catch (Exception e) diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs index f53b0bfff092..7eacccc8d5e2 100644 --- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs +++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs @@ -1,4 +1,5 @@ using System; +using System.Data; using System.Globalization; using System.Linq; using System.Web.Security; @@ -9,11 +10,13 @@ using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; +using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Stubs; @@ -98,10 +101,21 @@ public void Can_Mock_UmbracoApiController_Dependencies_With_Injected_UmbracoMapp { var umbracoContext = TestObjects.GetUmbracoContextMock(); + var scopeProvider = new Mock(); + scopeProvider + .Setup(x => x.CreateScope( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Mock.Of); + var membershipHelper = new MembershipHelper(umbracoContext.HttpContext, Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of()); var umbracoHelper = new UmbracoHelper(Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), membershipHelper); - var umbracoMapper = new UmbracoMapper(new MapDefinitionCollection(new[] { Mock.Of() })); - + var umbracoMapper = new UmbracoMapper(new MapDefinitionCollection(new[] { Mock.Of() }), scopeProvider.Object); + // ReSharper disable once UnusedVariable var umbracoApiController = new FakeUmbracoApiController(Mock.Of(), Mock.Of(), Mock.Of(), ServiceContext.CreatePartial(), AppCaches.NoCache, Mock.Of(), Mock.Of(), umbracoHelper, umbracoMapper); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js index 36eeb173d69e..ea8b5e81738c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js @@ -16,7 +16,7 @@ function UmbLoginController($scope, $location, currentUserResource, formHelper, mediaHelper, umbRequestHelper, Upload, localizationService, userService, externalLoginInfo, externalLoginInfoService, - resetPasswordCodeInfo, $timeout, authResource, $q, $route) { + resetPasswordCodeInfo, authResource, $q) { const vm = this; @@ -72,6 +72,7 @@ vm.loginSubmit = loginSubmit; vm.requestPasswordResetSubmit = requestPasswordResetSubmit; vm.setPasswordSubmit = setPasswordSubmit; + vm.newPasswordKeyUp = newPasswordKeyUp; vm.labels = {}; localizationService.localizeMany([ vm.usernameIsEmail ? "general_email" : "general_username", @@ -362,6 +363,9 @@ }); } + function newPasswordKeyUp(event) { + vm.passwordVal = event.target.value; + } //// diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbpasswordtip.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbpasswordtip.directive.js new file mode 100644 index 000000000000..86e1d3d32f39 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbpasswordtip.directive.js @@ -0,0 +1,71 @@ +(function () { + 'use strict'; + + angular + .module('umbraco.directives') + .component('umbPasswordTip', { + controller: UmbPasswordTipController, + controllerAs: 'vm', + template: + '{{vm.passwordTip}}', + bindings: { + passwordVal: "<", + minPwdLength: "<", + minPwdNonAlphaNum: "<" + } + }); + + function UmbPasswordTipController(localizationService) { + + let defaultMinPwdLength = Umbraco.Sys.ServerVariables.umbracoSettings.minimumPasswordLength; + let defaultMinPwdNonAlphaNum = Umbraco.Sys.ServerVariables.umbracoSettings.minimumPasswordNonAlphaNum; + + var vm = this; + vm.$onInit = onInit; + vm.$onChanges = onChanges; + + function onInit() { + if (vm.minPwdLength === undefined) { + vm.minPwdLength = defaultMinPwdLength; + } + + if (vm.minPwdNonAlphaNum === undefined) { + vm.minPwdNonAlphaNum = defaultMinPwdNonAlphaNum; + } + + if (vm.minPwdNonAlphaNum > 0) { + localizationService.localize('user_newPasswordFormatNonAlphaTip', [vm.minPwdNonAlphaNum]).then(data => { + vm.passwordNonAlphaTip = data; + updatePasswordTip(0); + }); + } else { + vm.passwordNonAlphaTip = ''; + updatePasswordTip(0); + } + } + + function onChanges(simpleChanges) { + if (simpleChanges.passwordVal) { + if (simpleChanges.passwordVal.currentValue) { + updatePasswordTip(simpleChanges.passwordVal.currentValue.length); + } else { + updatePasswordTip(0); + } + } + } + + const updatePasswordTip = passwordLength => { + const remainingLength = vm.minPwdLength - passwordLength; + if (remainingLength > 0) { + localizationService.localize('user_newPasswordFormatLengthTip', [remainingLength]).then(data => { + vm.passwordTip = data; + if (vm.passwordNonAlphaTip) { + vm.passwordTip += `
${vm.passwordNonAlphaTip}`; + } + }); + } else { + vm.passwordTip = vm.passwordNonAlphaTip; + } + } + } +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js index 3e227bfcb3c5..71bf151b89e2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js @@ -202,6 +202,9 @@ splitViewChanged(); unbindSplitViewRequest(); } + + // if split view was never closed, the listener is not disposed when changing nodes - this unbinds it + $scope.$on('$destroy', () => unbindSplitViewRequest()); /** * Changes the currently selected variant diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/prevententersubmit.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/prevententersubmit.directive.js index 355b02216f5f..62334387cb38 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/prevententersubmit.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/prevententersubmit.directive.js @@ -17,7 +17,7 @@ angular.module("umbraco.directives") } $(element).on("keypress", function (event) { - if (event.which === 13) { + if (event.which === 13 && enabled === true) { event.preventDefault(); } }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 569f49b88a48..f7cd32217e11 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -29,10 +29,6 @@ var defaultFocusedElement = getAutoFocusElement(focusableElements); var firstFocusableElement = focusableElements[0]; var lastFocusableElement = focusableElements[focusableElements.length -1]; - - // We need to add the tabbing-active class in order to highlight the focused button since the default style is - // outline: none; set in the stylesheet specifically - bodyElement.classList.add('tabbing-active'); // If there is no default focused element put focus on the first focusable element in the nodelist if(defaultFocusedElement === null ){ diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js index efbc384cb45a..2e9f15913cb4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js @@ -24,7 +24,7 @@ @param {boolean} model Set to true or false to set the checkbox to checked or unchecked. @param {string} inputId Set the id of the checkbox. @param {string} text Set the text for the checkbox label. -@param {string} labelKey Set a dictinary/localization string for the checkbox label +@param {string} labelKey Set a dictionary/localization string for the checkbox label @param {callback} onChange Callback when the value of the checkbox change by interaction. @param {boolean} autoFocus Add autofocus to the input field @param {boolean} preventSubmitOnEnter Set the enter prevent directive or not @@ -42,13 +42,15 @@ vm.change = change; function onInit() { - vm.inputId = vm.inputId || "umb-check_" + String.CreateGuid(); + vm.inputId = vm.inputId || "umb-search-filter_" + String.CreateGuid(); + vm.autoFocus = Object.toBoolean(vm.autoFocus) === true; + vm.preventSubmitOnEnter = Object.toBoolean(vm.preventSubmitOnEnter) === true; // If a labelKey is passed let's update the returned text if it's does not contain an opening square bracket [ if (vm.labelKey) { localizationService.localize(vm.labelKey).then(function (data) { - if(data.indexOf('[') === -1){ - vm.text = data; + if (data.indexOf('[') === -1){ + vm.text = data; } }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js index b1c860812405..7a10ff51b5a5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js @@ -11,24 +11,27 @@ Use this directive to render a tabs navigation.
     
- - - - -
-
Content of tab 1
-
-
-
Content of tab 2
-
-
- - + + + + + + + +
+
Content of tab 1
+
+
+
Content of tab 2
+
+
+ +
@@ -37,7 +40,7 @@ Use this directive to render a tabs navigation. (function () { "use strict"; - function Controller() { + function Controller(eventsService) { var vm = this; @@ -62,7 +65,7 @@ Use this directive to render a tabs navigation. selectedTab.active = true; }; - eventsService.on("tab.tabChange", function(name, args){ + eventsService.on("app.tabChange", function(name, args){ console.log("args", args); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js index 3e2e7e362ef3..2ae17fdc6b09 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js @@ -15,6 +15,7 @@ function treeSearchBox(localizationService, searchService, $q) { datatypeKey: "@", hideSearchCallback: "=", searchCallback: "=", + inputId: "@", autoFocus: "=" }, restrict: "E", // restrict to an element diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js index 8cbdabbf7549..a9961a7579ab 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js @@ -11,6 +11,7 @@ vm.cancelChange = cancelChange; vm.showOldPass = showOldPass; vm.showCancelBtn = showCancelBtn; + vm.newPasswordKeyUp = newPasswordKeyUp; var unsubscribe = []; @@ -55,6 +56,11 @@ vm.config.minPasswordLength = 0; } + // Check non-alpha pwd settings for tooltip display + if (vm.config.minNonAlphaNumericChars === undefined) { + vm.config.minNonAlphaNumericChars = 0; + } + //set the model defaults if (!Utilities.isObject(vm.passwordValues)) { //if it's not an object then just create a new one @@ -152,6 +158,9 @@ return vm.config.disableToggle !== true && vm.config.hasPassword; }; + function newPasswordKeyUp(event) { + vm.passwordVal = event.target.value; + } } var component = { diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js index ea7f3a6d4cd2..9a05e3cd7fbf 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js @@ -193,7 +193,7 @@ angular.module('umbraco.mocks'). "defaultdialogs_recycleBinDeleting": "The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place", "defaultdialogs_recycleBinIsEmpty": "The recycle bin is now empty", "defaultdialogs_recycleBinWarning": "When items are deleted from the recycle bin, they will be gone forever", - "defaultdialogs_regexSearchError": "regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.", + "defaultdialogs_regexSearchError": "regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.", "defaultdialogs_regexSearchHelp": "Search for a regular expression to add validation to a form field. Exemple: 'email, 'zip-code' 'url'", "defaultdialogs_removeMacro": "Remove Macro", "defaultdialogs_requiredField": "Required Field", @@ -355,7 +355,7 @@ angular.module('umbraco.mocks'). "installer_databaseHeader": "Database configuration", "installer_databaseInstall": " Press the install button to install the Umbraco %0% database ", "installer_databaseInstallDone": "Umbraco %0% has now been copied to your database. Press Next to proceed.", - "installer_databaseNotFound": "

Database not found! Please check that the information in the 'connection string' of the \"web.config\" file is correct.

To proceed, please edit the 'web.config' file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named 'UmbracoDbDSN' and save the file.

Click the retry button when done.
More information on editing web.config here.

", + "installer_databaseNotFound": "

Database not found! Please check that the information in the 'connection string' of the \"web.config\" file is correct.

To proceed, please edit the 'web.config' file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named 'UmbracoDbDSN' and save the file.

Click the retry button when done.
More information on editing web.config here.

", "installer_databaseText": "To complete this step, you must know some information regarding your database server ('connection string').
Please contact your ISP if necessary. If you're installing on a local machine or server you might need information from your system administrator.", "installer_databaseUpgrade": "

Press the upgrade button to upgrade your database to Umbraco %0%

Don't worry - no content will be deleted and everything will continue working afterwards!

", "installer_databaseUpgradeDone": "Your database has been upgraded to the final version %0%.
Press Next to proceed. ", @@ -420,7 +420,7 @@ angular.module('umbraco.mocks'). "login_greeting6": "Happy friendly Friday", "login_greeting7": "Happy shiny Saturday", "login_instruction": "Log in below:", - "login_bottomText": "

© 2001 - %0%
Umbraco.org

", + "login_bottomText": "

© 2001 - %0%
Umbraco.org

", "main_dashboard": "Dashboard", "main_sections": "Sections", "main_tree": "Content", diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index 6acf7025467f..a3a5b1946dfa 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -432,13 +432,15 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca throw "args.id cannot be null"; } + var promise = localizationService.localize("contentType_moveFailed"); + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostMove"), { parentId: args.parentId, id: args.id }, { responseType: 'text' }), - 'Failed to move content'); + promise); }, /** @@ -475,13 +477,15 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca throw "args.id cannot be null"; } + var promise = localizationService.localize("contentType_copyFailed"); + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCopy"), { parentId: args.parentId, id: args.id }, { responseType: 'text' }), - 'Failed to copy content'); + promise); }, /** diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js index d194ae2c7324..e3fab86067a7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js @@ -208,7 +208,7 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter, locali throw "args.id cannot be null"; } - var promise = localizationService.localize("media_moveFailed"); + var promise = localizationService.localize("mediaType_moveFailed"); return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostMove"), @@ -230,7 +230,7 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter, locali throw "args.id cannot be null"; } - var promise = localizationService.localize("media_copyFailed"); + var promise = localizationService.localize("mediaType_copyFailed"); return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostCopy"), diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js index 2314fa6d6cc7..bf02d9618ec0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js @@ -3,7 +3,7 @@ * @name umbraco.resources.memberTypeResource * @description Loads in data for member types **/ -function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { +function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter, localizationService) { return { @@ -102,8 +102,29 @@ function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("memberTypeApiBaseUrl", "PostSave"), saveModel), 'Failed to save data for member type id ' + contentType.id); - } + }, + copy: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + var promise = localizationService.localize("memberType_copyFailed"); + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("memberTypeApiBaseUrl", "PostCopy"), + { + parentId: args.parentId, + id: args.id + }, { responseType: 'text' }), + promise); + } }; } angular.module('umbraco.resources').factory('memberTypeResource', memberTypeResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index 14643dc9cd39..f9ebba00ea4d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -572,13 +572,15 @@ * Method for opening an item in a list view for editing. * * @param {Object} item The item to edit + * @param {Object} scope The scope with options */ function editItem(item, scope) { + if (!item.editPath) { return; } - if (scope.options.useInfiniteEditor) + if (scope && scope.options && scope.options.useInfiniteEditor) { var editorModel = { id: item.id, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index 0a4009264d5e..2b5447cdf681 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -252,10 +252,10 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe //each item has a property alias and the file object, we'll ensure that the alias is suffixed to the key // so we know which property it belongs to on the server side var file = args.files[f]; - var fileKey = "file_" + file.alias + "_" + (file.culture ? file.culture : "") + "_" + (file.segment ? file.segment : ""); + var fileKey = "file_" + (file.alias || '').replace(/_/g, '\\_') + "_" + (file.culture ? file.culture.replace(/_/g, '\\_') : "") + "_" + (file.segment ? file.segment.replace(/_/g, '\\_') : ""); if (Utilities.isArray(file.metaData) && file.metaData.length > 0) { - fileKey += ("_" + file.metaData.join("_")); + fileKey += ("_" + _.map(file.metaData, x => ('' + x).replace(/_/g, '\\_')).join("_")); } formData.append(fileKey, file.file); } diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js index 74858d652ebb..d3ab9e519cd5 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js +++ b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js @@ -19,8 +19,8 @@ angular.module("umbraco.install").factory('installerService', function ($rootSco "Over 500 000 websites are currently powered by Umbraco", "At least 2 people have named their cat 'Umbraco'", "On an average day more than 1000 people download Umbraco", - "umbraco.tv is the premier source of Umbraco video tutorials to get you started", - "You can find the world's friendliest CMS community at our.umbraco.com", + "umbraco.tv is the premier source of Umbraco video tutorials to get you started", + "You can find the world's friendliest CMS community at our.umbraco.com", "You can become a certified Umbraco developer by attending one of the official courses", "Umbraco works really well on tablets", "You have 100% control over your markup and design when crafting a website in Umbraco", @@ -30,7 +30,7 @@ angular.module("umbraco.install").factory('installerService', function ($rootSco "At least 4 people have the Umbraco logo tattooed on them", "'Umbraco' is the Danish name for an allen key", "Umbraco has been around since 2005, that's a looong time in IT", - "More than 700 people from all over the world meet each year in Denmark in May for our annual conference CodeGarden", + "More than 700 people from all over the world meet each year in Denmark in May for our annual conference CodeGarden", "While you are installing Umbraco someone else on the other side of the planet is probably doing it too", "You can extend Umbraco without modifying the source code using either JavaScript or C#", "Umbraco has been installed in more than 198 countries" diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/upgrade.html b/src/Umbraco.Web.UI.Client/src/installer/steps/upgrade.html index 472ceb713503..8ea69b3ee44c 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/upgrade.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/upgrade.html @@ -10,7 +10,7 @@

Upgrading Umbraco

To compare versions and read a report of changes between versions, use the View Report button below.

- View Report + View Report

Simply click continue below to be guided through the rest of the upgrade. diff --git a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less index 939366d5aca7..1f1c2c0e724d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less @@ -15,6 +15,7 @@ right: 0; border-radius: 3px; box-shadow: 0 0 2px 0px @ui-outline, inset 0 0 2px 2px @ui-outline; + pointer-events: none; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/buttons.less b/src/Umbraco.Web.UI.Client/src/less/buttons.less index c446a0242400..d1a426f81837 100644 --- a/src/Umbraco.Web.UI.Client/src/less/buttons.less +++ b/src/Umbraco.Web.UI.Client/src/less/buttons.less @@ -65,15 +65,22 @@ // -------------------------------------------------- .btn-reset { - padding: 0; - margin: 0; - border: none; + padding: 0; + margin: 0; + border: none; background: none; - color: currentColor; + color: currentColor; font-family: @baseFontFamily; font-size: @baseFontSize; line-height: @baseLineHeight; - cursor: pointer; + cursor: pointer; + + // Disabled state + &.disabled, + &[disabled], + &:disabled:hover { + cursor: default; + } } // Button Sizes diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less index 9d2782f184c8..ce9286e5f5da 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less @@ -69,7 +69,6 @@ button.umb-variant-switcher__toggle { .umb-variant-switcher__expand { color: @ui-action-discreet-type; - margin-top: 3px; margin-left: 5px; margin-right: -5px; transition: color 0.2s ease-in-out; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less index b96d3e856917..b38f5937c727 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less @@ -21,11 +21,12 @@ html .umb-search-filter { // "icon-search" class it kept for backward compatibility .umb-icon, .icon-search { - color: #d8d7d9; + color: @gray-8; position: absolute; top: 0; bottom: 0; left: 10px; margin: auto 0; + pointer-events: none; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index f38ba8f8067b..3782fca695c2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -308,7 +308,14 @@ select[size] { input[type="file"], input[type="radio"], input[type="checkbox"] { - .umb-outline(); + &:focus { + border-color: @inputBorderFocus; + outline: 0; + + .tabbing-active & { + outline: 2px solid @ui-outline; + } + } } @@ -582,19 +589,21 @@ table.domains .help-inline { } } .add-on { - display: inline-block; + display: inline-flex; + align-items: center; + justify-content: center; width: auto; height: 22px; min-width: 18px; - padding: 5px 6px 3px 6px; + padding: 4px 6px; font-size: @baseFontSize; font-weight: normal; line-height: @baseLineHeight; text-align: center; - //text-shadow: 0 1px 0 @white; background-color: @white; border: 1px solid @inputBorder; color: @ui-option-type; + &:hover { border-color:@inputBorderFocus; color: @ui-option-type-hover; diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 31bb8484c4a4..66afbfd73f2e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -402,6 +402,10 @@ table thead button:focus{ /* UI interactions */ +.ui-sortable-handle { + cursor: move; +} + .umb-table tbody.ui-sortable tr { cursor:pointer; diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index a036267c8587..cc87a0edf54e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -349,6 +349,7 @@ .umb-panel-header-icon { cursor: pointer; + font-size: 2rem; margin-right: 5px; margin-top: -6px; height: 50px; 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 0d8f270f1b18..f5e652aa3d85 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -838,22 +838,25 @@ // // Date/time picker // -------------------------------------------------- -.bootstrap-datetimepicker-widget .btn{padding: 0;} -.bootstrap-datetimepicker-widget .picker-switch .btn{ background: none; border: none;} -.umb-datepicker .input-append .add-on{cursor: pointer;} -.umb-datepicker .input-append .on-top { - border: 0 none; +.bootstrap-datetimepicker-widget .btn {padding: 0;} +.bootstrap-datetimepicker-widget .picker-switch .btn { background: none; border: none;} +.umb-datepicker .input-append .btn-clear { + border: none; position: absolute; margin-left: -31px; margin-top: 1px; - display: inline-block; - padding: 5px 6px 3px 6px; + display: inline-flex; + align-items: center; + justify-content: center; + height: 30px; + padding: 4px 6px; font-size: @baseFontSize; font-weight: normal; line-height: @baseLineHeight; text-align: center; background-color: @white; color: @ui-option-type; + &:hover { color: @ui-option-type-hover; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html index 093e69b5ed52..0a2a8223e953 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html @@ -16,7 +16,7 @@

- + + ng-disabled="model.target.id || model.target.udi" + id="urlLinkPicker"/> - + + ng-model="model.target.anchor" + id="anchor"/> @@ -41,19 +43,21 @@
- + + ng-model="model.target.name" + id="nodeNameLinkPicker"/> - + + text="{{vm.labels.openInNewWindow}}" + input-id="openInNewWindow"> @@ -61,36 +65,35 @@
Link to page
- + +
- - +
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js index 40338f2dcadf..1701553efceb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js @@ -1,12 +1,13 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEditorHelper, macroService, formHelper, localizationService) { $scope.macros = []; + $scope.a11yInfo = ""; $scope.model.selectedMacro = null; $scope.model.macroParams = []; - + $scope.displayA11YMessageForFilter = displayA11YMessageForFilter; $scope.wizardStep = "macroSelect"; $scope.noMacroParams = false; - + $scope.model.searchTerm = ""; function onInit() { if (!$scope.model.title) { localizationService.localize("defaultdialogs_selectMacro").then(function (value) { @@ -49,6 +50,7 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi $scope.model.submit($scope.model); } else { $scope.wizardStep = 'macroSelect'; + displayA11yMessages($scope.macros); } } else { @@ -95,6 +97,28 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi }); } + function displayA11yMessages(macros) { + if ($scope.noMacroParams || !macros || macros.length === 0) + localizationService.localize("general_searchNoResult").then(function (value) { + $scope.a11yInfo = value; + }); + else if (macros) { + if (macros.length === 1) { + localizationService.localize("treeSearch_searchResult").then(function(value) { + $scope.a11yInfo = "1 " + value; + }); + } else { + localizationService.localize("treeSearch_searchResults").then(function (value) { + $scope.a11yInfo = macros.length + " " + value; + }); + } + } + } + + function displayA11YMessageForFilter() { + var macros = _.filter($scope.macros, v => v.name.toLowerCase().includes($scope.model.searchTerm.toLowerCase())); + displayA11yMessages(macros); + } //here we check to see if we've been passed a selected macro and if so we'll set the //editor to start with parameter editing if ($scope.model.dialogData && $scope.model.dialogData.macroData) { @@ -141,10 +165,11 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi //we don't have a pre-selected macro so ensure the correct step is set $scope.wizardStep = 'macroSelect'; } + displayA11yMessages($scope.macros); }); onInit(); - + } angular.module("umbraco").controller("Umbraco.Overlays.MacroPickerController", MacroPickerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html index 33d7a471a563..8bda49b328f5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html @@ -16,18 +16,18 @@
- - + + - +

    -
  • +
+ position="center"> There are no macros available to insert @@ -53,7 +53,7 @@
{{model.selectedMacro.name}}
  • - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js index a7021b2867f5..33d526c3cf52 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js @@ -1,8 +1,8 @@ (function () { "use strict"; - function UserPickerController($scope, usersResource, localizationService, eventsService) { - + function UserPickerController($scope, entityResource, localizationService, eventsService) { + var vm = this; vm.users = []; @@ -102,17 +102,9 @@ vm.loading = true; // Get users - usersResource.getPagedResults(vm.usersOptions).then(function (users) { - - vm.users = users.items; - - vm.usersOptions.pageNumber = users.pageNumber; - vm.usersOptions.pageSize = users.pageSize; - vm.usersOptions.totalItems = users.totalItems; - vm.usersOptions.totalPages = users.totalPages; - + entityResource.getAll("User").then(function (data) { + vm.users = data; preSelect($scope.model.selection, vm.users); - vm.loading = false; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html index 1e570b4af6b6..c4b6a4a2ed5d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html @@ -226,7 +226,8 @@

    {{greeting}}

    - + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index 6978672e9984..6e3363351246 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -96,6 +96,7 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html index d6fde290907f..ab21654f9100 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html @@ -2,27 +2,14 @@
    - - - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-box.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-box.html index 77498cd00724..054472e4b65b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-box.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-box.html @@ -2,6 +2,7 @@ - - Sorry, we can not find what you are looking for. - - -
      -
    • -
        -
      • -
        - -
        + +

        Sorry, we can not find what you are looking for.

        +
        +

        1 item returned

        +

        {{results.length}} items returned

        +
          +
        • +
            +
          • +
            + +
            +
          • +
        • -
        -
      • -
      +
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html index 974f8d6b4ee2..273f56d256ed 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html @@ -43,12 +43,14 @@ required val-server-field="password" ng-minlength="{{vm.config.minPasswordLength}}" - no-dirty-check /> + no-dirty-check + ng-keyup="vm.newPasswordKeyUp($event)"/> Required Minimum {{vm.config.minPasswordLength}} characters {{changePasswordForm.password.errorMsg}} + diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardvideos.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardvideos.html index 96d6a3f40a1f..670fae2f6ee1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardvideos.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardvideos.html @@ -1,5 +1,5 @@

    Hours of Umbraco training videos are only a click away

    -

    Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

    +

    Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

    - + {{key}} - {{val}} + {{values | umbCmsJoinArray:', '}} diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html index 8b81462ad50a..824527be34c7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html @@ -20,7 +20,7 @@
    {{source.name}} was copied underneath {{target.name}}
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/export.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/export.html index 0b9feb3fb6b3..b9aa4d9d3a01 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/export.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/export.html @@ -1,8 +1,8 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html index fe0bde7f1fbb..b623b6131db1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html @@ -20,7 +20,7 @@
    {{source.name}} was moved underneath {{target.name}}
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html index 9c21f623b532..58968c9dfa4e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html @@ -20,7 +20,7 @@
    {{source.name}} was copied underneath {{target.name}}
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html index 5225a41a0d66..6bb1b6fa105d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html @@ -20,7 +20,7 @@
    {{source.name}} was moved underneath {{target.name}}
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/membertypes/copy.controller.js new file mode 100644 index 000000000000..aa94b4bd04e4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/copy.controller.js @@ -0,0 +1,61 @@ +angular.module("umbraco") + .controller("Umbraco.Editors.MemberTypes.CopyController", + function ($scope, memberTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + + $scope.dialogTreeApi = {}; + $scope.source = _.clone($scope.currentNode); + + function nodeSelectHandler(args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.copy = function () { + + $scope.busy = true; + $scope.error = false; + + memberTypeResource.copy({ parentId: $scope.target.id, id: $scope.source.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the copied content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was copied!!) + + navigationService.syncTree({ tree: "memberTypes", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "memberTypes", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + }); + }; + + $scope.onTreeInit = function () { + $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); + }; + + $scope.close = function() { + navigationService.hideDialog(); + }; + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/membertypes/copy.html new file mode 100644 index 000000000000..fb7c6b55846d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/copy.html @@ -0,0 +1,53 @@ +
    + +
    +
    + +

    + Select the folder to copy {{source.name}} to in the tree structure below +

    + + + +
    +
    +
    {{error.errorMsg}}
    +
    {{error.data.message}}
    +
    +
    + +
    +
    + {{source.name}} was copied underneath {{target.name}} +
    + +
    + +
    + +
    + + +
    + +
    +
    +
    + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js index ab7f5c66e04e..e80aad64f9c9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js @@ -36,6 +36,9 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.ChangePasswordCont if (!$scope.model.config || $scope.model.config.minPasswordLength === undefined) { $scope.model.config.minPasswordLength = 0; } + if (!$scope.model.config || $scope.model.config.minNonAlphaNumericChars === undefined) { + $scope.model.config.minNonAlphaNumericChars = 0; + } //set the model defaults if (!Utilities.isObject($scope.model.value)) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html index 9501a6631b41..f5ac69b9b8e6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html @@ -19,12 +19,12 @@ ng-required="model.validation.mandatory" val-server="value" class="datepickerinput" /> - -
    @@ -32,7 +32,7 @@
    -
    +

    {{mandatoryMessage}}

    {{datePickerForm.datepicker.errorMsg}}

    Invalid date

    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html index 26ec22df8d54..3ae03a2d7bfd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html @@ -1,6 +1,7 @@
    - -
    +

    {{mandatoryMessage}}

    Invalid email

    {{emailFieldForm.textbox.errorMsg}}

    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index 716ca405c12e..94ea4b8604ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -1,9 +1,9 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.MediaController", - function ($scope, userService, editorService, localizationService) { - - $scope.thumbnailUrl = getThumbnailUrl(); - + function ($scope, userService, editorService, localizationService) { + + $scope.thumbnailUrl = getThumbnailUrl(); + if (!$scope.model.config.startNodeId) { if ($scope.model.config.ignoreUserStartNodes === true) { $scope.model.config.startNodeId = -1; @@ -29,16 +29,16 @@ angular.module("umbraco") onlyImages: true, dataTypeKey: $scope.model.dataTypeKey, submit: model => { - updateControlValue(model.selection[0]); + updateControlValue(model.selection[0]); editorService.close(); }, - close: () => editorService.close() + close: () => editorService.close() }; editorService.mediaPicker(mediaPicker); }; - $scope.editImage = function() { + $scope.editImage = function() { const mediaCropDetailsConfig = { size: 'small', @@ -47,17 +47,17 @@ angular.module("umbraco") updateControlValue(model.target); editorService.close(); }, - close: () => editorService.close() + close: () => editorService.close() }; localizationService.localize('defaultdialogs_editSelectedMedia').then(value => { mediaCropDetailsConfig.title = value; editorService.mediaCropDetails(mediaCropDetailsConfig); - }); + }); } - + /** - * + * */ function getThumbnailUrl() { @@ -94,19 +94,15 @@ angular.module("umbraco") return url; } - + return null; } /** - * - * @param {object} selectedImage + * + * @param {object} selectedImage */ function updateControlValue(selectedImage) { - - const doGetThumbnail = $scope.control.value.focalPoint !== selectedImage.focalPoint - || $scope.control.value.image !== selectedImage.image; - // we could apply selectedImage directly to $scope.control.value, // but this allows excluding fields in future if needed $scope.control.value = { @@ -118,10 +114,6 @@ angular.module("umbraco") caption: selectedImage.caption, altText: selectedImage.altText }; - - - if (doGetThumbnail) { - $scope.thumbnailUrl = getThumbnailUrl(); - } - } + $scope.thumbnailUrl = getThumbnailUrl(); + } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index 50146a4c36cd..15f5ceaa88c5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -141,9 +141,9 @@ angular.module("umbraco") over: function (event, ui) { var area = event.target.getScope_HackForSortable().area; - var allowedEditors = area.allowed; + var allowedEditors = area.$allowedEditors.map(e => e.alias); - if (($.inArray(ui.item[0].getScope_HackForSortable().control.editor.alias, allowedEditors) < 0 && allowedEditors) || + if (($.inArray(ui.item[0].getScope_HackForSortable().control.editor.alias, allowedEditors) < 0) || (startingArea != area && area.maxItems != '' && area.maxItems > 0 && area.maxItems < area.controls.length + 1)) { $scope.$apply(function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html index 5c1079040026..24a8c3369692 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html @@ -9,10 +9,15 @@ aria-required="{{model.validation.mandatory}}" id="{{model.alias}}" val-server="value" - fix-number min="{{model.config.min}}" max="{{model.config.max}}" step="{{model.config.step}}" /> + min="{{model.config.min}}" + max="{{model.config.max}}" + step="{{model.config.step}}" + ng-step="model.config.step" + fix-number /> - + Not a number + Not a valid numeric step size {{integerFieldForm.integerField.errorMsg}} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html index 42597f0c8201..7d863f6730c9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html index 4527458d163a..295345a8278b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html @@ -6,20 +6,20 @@
    - +
    - -
    - -
    -
    @@ -32,12 +32,12 @@
    -
    +
    (blank) -

    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/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 03f462fb9e36..ae141e5408bd 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -37,6 +37,7 @@ + diff --git a/src/Umbraco.Web/Dashboards/ContentDashboard.cs b/src/Umbraco.Web/Dashboards/ContentDashboard.cs index 0cd96f738c88..260eb8baf929 100644 --- a/src/Umbraco.Web/Dashboards/ContentDashboard.cs +++ b/src/Umbraco.Web/Dashboards/ContentDashboard.cs @@ -1,15 +1,21 @@ -using Umbraco.Core; +using System.Collections.Generic; +using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Dashboards; +using Umbraco.Core.Services; namespace Umbraco.Web.Dashboards { [Weight(10)] public class ContentDashboard : IDashboard { + private readonly IContentDashboardSettings _dashboardSettings; + private readonly IUserService _userService; + private IAccessRule[] _accessRulesFromConfig; + public string Alias => "contentIntro"; - public string[] Sections => new [] { "content" }; + public string[] Sections => new[] { "content" }; public string View => "views/dashboard/default/startupdashboardintro.html"; @@ -17,13 +23,54 @@ public IAccessRule[] AccessRules { get { - var rules = new IAccessRule[] + var rules = AccessRulesFromConfig; + + if (rules.Length == 0) { - new AccessRule {Type = AccessRuleType.Deny, Value = Constants.Security.TranslatorGroupAlias}, - new AccessRule {Type = AccessRuleType.Grant, Value = Constants.Security.AdminGroupAlias} - }; + rules = new IAccessRule[] + { + new AccessRule {Type = AccessRuleType.Deny, Value = Constants.Security.TranslatorGroupAlias}, + new AccessRule {Type = AccessRuleType.Grant, Value = Constants.Security.AdminGroupAlias} + }; + } + return rules; } } + + private IAccessRule[] AccessRulesFromConfig + { + get + { + if (_accessRulesFromConfig is null) + { + var rules = new List(); + + if (_dashboardSettings.AllowContentDashboardAccessToAllUsers) + { + var allUserGroups = _userService.GetAllUserGroups(); + + foreach (var userGroup in allUserGroups) + { + rules.Add(new AccessRule + { + Type = AccessRuleType.Grant, + Value = userGroup.Alias + }); + } + } + + _accessRulesFromConfig = rules.ToArray(); + } + + return _accessRulesFromConfig; + } + } + + public ContentDashboard(IContentDashboardSettings dashboardSettings, IUserService userService) + { + _dashboardSettings = dashboardSettings; + _userService = userService; + } } } diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 42b5186c03aa..6ec9ac4f90bb 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -57,7 +57,7 @@ internal Dictionary BareMinimumServerVariables() var keepOnlyKeys = new Dictionary { {"umbracoUrls", new[] {"authenticationApiBaseUrl", "serverVarsJs", "externalLoginsUrl", "currentUserApiBaseUrl", "iconApiBaseUrl"}}, - {"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage", "loginLogoImage", "canSendRequiredEmail", "usernameIsEmail"}}, + {"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage", "loginLogoImage", "canSendRequiredEmail", "usernameIsEmail", "minimumPasswordLength", "minimumPasswordNonAlphaNum"}}, {"application", new[] {"applicationPath", "cacheBuster"}}, {"isDebuggingEnabled", new string[] { }}, {"features", new [] {"disabledFeatures"}} @@ -100,6 +100,8 @@ internal Dictionary BareMinimumServerVariables() /// internal Dictionary GetServerVariables() { + var userMembershipProvider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider(); + var defaultVals = new Dictionary { { @@ -357,6 +359,8 @@ internal Dictionary GetServerVariables() {"showUserInvite", EmailSender.CanSendRequiredEmail}, {"canSendRequiredEmail", EmailSender.CanSendRequiredEmail}, {"showAllowSegmentationForDocumentTypes", false}, + {"minimumPasswordLength", userMembershipProvider.MinRequiredPasswordLength}, + {"minimumPasswordNonAlphaNum", userMembershipProvider.MinRequiredNonAlphanumericCharacters}, } }, { diff --git a/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs b/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs index e0d39b5f65ab..75060d059a5a 100644 --- a/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs +++ b/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs @@ -1,5 +1,9 @@ -using System.Net; +using System.Collections.Generic; +using System.Linq; +using System.Net; using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; using System.Web.Http; using System.Web.Http.Controllers; using Umbraco.Core; @@ -17,6 +21,8 @@ namespace Umbraco.Web.Editors.Binders ///
    internal static class ContentModelBinderHelper { + private const char _escapeChar = '\\'; + public static TModelSave BindModelFromMultipartRequest(HttpActionContext actionContext, ModelBindingContext bindingContext) where TModelSave : IHaveUploadedFiles { @@ -30,6 +36,7 @@ public static TModelSave BindModelFromMultipartRequest(HttpActionCon //The name that has been assigned in JS has 2 or more parts. The second part indicates the property id // for which the file belongs, the remaining parts are just metadata that can be used by the property editor. var parts = file.Headers.ContentDisposition.Name.Trim(Constants.CharArrays.DoubleQuote).Split(Constants.CharArrays.Underscore); + if (parts.Length < 2) { var response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest); diff --git a/src/Umbraco.Web/Editors/CodeFileController.cs b/src/Umbraco.Web/Editors/CodeFileController.cs index 409cded78136..a6d142a6ea32 100644 --- a/src/Umbraco.Web/Editors/CodeFileController.cs +++ b/src/Umbraco.Web/Editors/CodeFileController.cs @@ -638,7 +638,10 @@ private bool IsDirectory(string virtualPath, string systemDirectory) { var path = IOHelper.MapPath(systemDirectory + "/" + virtualPath); var dirInfo = new DirectoryInfo(path); - return dirInfo.Attributes == FileAttributes.Directory; + + // If you turn off indexing in Windows this will have the attribute: + // `FileAttributes.Directory | FileAttributes.NotContentIndexed` + return (dirInfo.Attributes & FileAttributes.Directory) != 0; } // this is an internal class for passing stylesheet data from the client to the controller while editing diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index da620eb5ac3f..97db8818f232 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Core.Dashboards; +using Umbraco.Core.Models; using Umbraco.Web.Services; namespace Umbraco.Web.Editors @@ -52,8 +53,9 @@ public async Task GetRemoteDashboardContent(string section, string base var allowedSections = string.Join(",", user.AllowedSections); var language = user.Language; var version = UmbracoVersion.SemanticVersion.ToSemanticString(); + var isAdmin = user.IsAdmin(); - var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}", section, allowedSections, language, version); + var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}&admin={4}", section, allowedSections, language, version, isAdmin); var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section; var content = AppCaches.RuntimeCache.GetCacheItem(key); diff --git a/src/Umbraco.Web/Editors/ExamineManagementController.cs b/src/Umbraco.Web/Editors/ExamineManagementController.cs index 49599bc8b902..132cc25404c7 100644 --- a/src/Umbraco.Web/Editors/ExamineManagementController.cs +++ b/src/Umbraco.Web/Editors/ExamineManagementController.cs @@ -25,7 +25,6 @@ public class ExamineManagementController : UmbracoAuthorizedJsonController private readonly IAppPolicyCache _runtimeCache; private readonly IndexRebuilder _indexRebuilder; - public ExamineManagementController(IExamineManager examineManager, ILogger logger, AppCaches appCaches, IndexRebuilder indexRebuilder) @@ -79,14 +78,11 @@ public SearchResults GetSearchResults(string searcherName, string query, int pag { Id = x.Id, Score = x.Score, - //order the values by key - Values = new Dictionary(x.Values.OrderBy(y => y.Key).ToDictionary(y => y.Key, y => y.Value)) + Values = x.AllValues.OrderBy(y => y.Key).ToDictionary(y => y.Key, y => y.Value) }) }; } - - /// /// Check if the index has been rebuilt /// @@ -113,7 +109,6 @@ public ExamineIndexModel PostCheckRebuildIndex(string indexName) return found != null ? null : CreateModel(index); - } /// @@ -167,8 +162,6 @@ public HttpResponseMessage PostRebuildIndex(string indexName) } } - - private ExamineIndexModel CreateModel(IIndex index) { var indexName = index.Name; @@ -182,11 +175,13 @@ private ExamineIndexModel CreateModel(IIndex index) } var isHealth = indexDiag.IsHealthy(); + var properties = new Dictionary { [nameof(IIndexDiagnostics.DocumentCount)] = indexDiag.DocumentCount, [nameof(IIndexDiagnostics.FieldCount)] = indexDiag.FieldCount, }; + foreach (var p in indexDiag.Metadata) properties[p.Key] = p.Value; @@ -198,7 +193,6 @@ private ExamineIndexModel CreateModel(IIndex index) CanRebuild = _indexRebuilder.CanRebuild(index) }; - return indexerModel; } @@ -211,7 +205,6 @@ private HttpResponseMessage ValidateSearcher(string searcherName, out ISearcher return Request.CreateResponse(HttpStatusCode.OK); } - //if we didn't find anything try to find it by an explicitly declared searcher if (_examineManager.TryGetSearcher(searcherName, out searcher)) return Request.CreateResponse(HttpStatusCode.OK); diff --git a/src/Umbraco.Web/Editors/KeepAliveController.cs b/src/Umbraco.Web/Editors/KeepAliveController.cs index 23815e1bbe06..f29ee6c60a73 100644 --- a/src/Umbraco.Web/Editors/KeepAliveController.cs +++ b/src/Umbraco.Web/Editors/KeepAliveController.cs @@ -1,14 +1,12 @@ using System.Runtime.Serialization; using System.Web.Http; -using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Editors { public class KeepAliveController : UmbracoApiController { - [OnlyLocalRequests] + [HttpHead] [HttpGet] public KeepAlivePingResult Ping() { diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 4bfea76edad1..2e665350e7d9 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -237,6 +237,18 @@ public MemberTypeDisplay PostSave(MemberTypeSave contentTypeSave) return display; } + /// + /// Copy the member type + /// + /// + /// + public HttpResponseMessage PostCopy(MoveOrCopy copy) + { + return PerformCopy( + copy, + getContentType: i => Services.MemberTypeService.Get(i), + doCopy: (type, i) => Services.MemberTypeService.Copy(type, i)); + } } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs index fd76b9d4869e..33cf89b2e4be 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs @@ -49,7 +49,7 @@ private HealthCheckStatus CheckForHeaders() { var message = string.Empty; var success = false; - var url = _runtime.ApplicationUrl; + var url = _runtime.ApplicationUrl.GetLeftPart(UriPartial.Authority); // Access the site home page and check for the headers var request = WebRequest.Create(url); @@ -69,7 +69,7 @@ private HealthCheckStatus CheckForHeaders() } catch (Exception ex) { - message = _textService.Localize("healthcheck/httpsCheckInvalidUrl", new[] { url.ToString(), ex.Message }); + message = _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url.ToString(), ex.Message }); } var actions = new List(); diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs index 98f8a83c1dfc..83fafb79f8d5 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Net; using System.Security.Cryptography.X509Certificates; -using System.Web; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Services; using Umbraco.Web.HealthCheck.Checks.Config; @@ -21,14 +21,16 @@ public class HttpsCheck : HealthCheck private readonly ILocalizedTextService _textService; private readonly IRuntimeState _runtime; private readonly IGlobalSettings _globalSettings; + private readonly IContentSection _contentSection; private const string FixHttpsSettingAction = "fixHttpsSetting"; - public HttpsCheck(ILocalizedTextService textService, IRuntimeState runtime, IGlobalSettings globalSettings) + public HttpsCheck(ILocalizedTextService textService, IRuntimeState runtime, IGlobalSettings globalSettings, IContentSection contentSection) { _textService = textService; _runtime = runtime; _globalSettings = globalSettings; + _contentSection = contentSection; } /// @@ -65,12 +67,25 @@ private HealthCheckStatus CheckForValidCertificate() // Attempt to access the site over HTTPS to see if it HTTPS is supported // and a valid certificate has been configured var url = _runtime.ApplicationUrl.ToString().Replace("http:", "https:"); + var request = (HttpWebRequest) WebRequest.Create(url); - request.Method = "HEAD"; + request.AllowAutoRedirect = false; try { + var response = (HttpWebResponse)request.GetResponse(); + + // Check for 301/302 as a external login provider such as UmbracoID might be in use + if (response.StatusCode == HttpStatusCode.Moved || response.StatusCode == HttpStatusCode.Redirect) + { + // Reset request to use the static login background image + var absoluteLoginBackgroundImage = $"{url}/{_contentSection.LoginBackgroundImage}"; + + request = (HttpWebRequest)WebRequest.Create(absoluteLoginBackgroundImage); + response = (HttpWebResponse)request.GetResponse(); + } + if (response.StatusCode == HttpStatusCode.OK) { // Got a valid response, check now for if certificate expiring within 14 days diff --git a/src/Umbraco.Web/Models/ContentEditing/SearchResult.cs b/src/Umbraco.Web/Models/ContentEditing/SearchResult.cs index 1cdd539165b9..d33bc3530e89 100644 --- a/src/Umbraco.Web/Models/ContentEditing/SearchResult.cs +++ b/src/Umbraco.Web/Models/ContentEditing/SearchResult.cs @@ -16,6 +16,6 @@ public class SearchResult public int FieldCount => Values?.Count ?? 0; [DataMember(Name = "values")] - public IReadOnlyDictionary Values { get; set; } + public IReadOnlyDictionary> Values { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 862837381a24..f9eacd9e73d6 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -202,8 +202,8 @@ public IEnumerable GetReferences(object value) _richTextPropertyValueEditor.GetReferences(x.Value))) yield return umbracoEntityReference; - foreach (var umbracoEntityReference in mediaValues.SelectMany(x => - _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) + foreach (var umbracoEntityReference in mediaValues.Where(x => x.Value.HasValues) + .SelectMany(x => _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) yield return umbracoEntityReference; } } diff --git a/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs b/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs index de538793a5fe..2ea7b9e44eff 100644 --- a/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs @@ -28,7 +28,7 @@ public ListViewConfiguration() Layouts = new[] { new Layout { Name = "List", Icon = "icon-list", IsSystem = 1, Selected = true, Path = "views/propertyeditors/listview/layouts/list/list.html" }, - new Layout { Name = "grid", Icon = "icon-thumbnails-small", IsSystem = 1, Selected = true, Path = "views/propertyeditors/listview/layouts/grid/grid.html" } + new Layout { Name = "Grid", Icon = "icon-thumbnails-small", IsSystem = 1, Selected = true, Path = "views/propertyeditors/listview/layouts/grid/grid.html" } }; IncludeProperties = new [] diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 8aa3b69fb432..c851894149e7 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -173,8 +173,8 @@ public static T Value(this IPublishedContent content, string alias, string cu return value; // else... if we have a property, at least let the converter return its own - // vision of 'no value' (could be an empty enumerable) - otherwise, defaultValue - return property == null ? defaultValue : property.Value(culture, segment, defaultValue: defaultValue); + // vision of 'no value' (could be an empty enumerable) - otherwise, default + return property == null ? default : property.Value(culture, segment, fallback, defaultValue); } #endregion @@ -814,6 +814,64 @@ internal static IEnumerable EnumerateAncestors(this IPublishe #endregion + #region Axes: breadcrumbs + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified . + /// + /// The content. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified . + /// + public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) + { + return content.AncestorsOrSelf(andSelf, null).Reverse(); + } + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to . + /// + /// The content. + /// The minimum level. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to . + /// + public static IEnumerable Breadcrumbs(this IPublishedContent content, int minLevel, bool andSelf = true) + { + return content.AncestorsOrSelf(andSelf, n => n.Level >= minLevel).Reverse(); + } + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to the specified root content type . + /// + /// The root content type. + /// The content. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to the specified root content type . + /// + public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) + where T : class, IPublishedContent + { + static IEnumerable TakeUntil(IEnumerable source, Func predicate) + { + foreach (var item in source) + { + yield return item; + if (predicate(item)) + { + yield break; + } + } + } + + return TakeUntil(content.AncestorsOrSelf(andSelf, null), n => n is T).Reverse(); + } + + #endregion + #region Axes: descendants, descendants-or-self /// @@ -1271,15 +1329,37 @@ public static IEnumerable SiblingsAndSelf(this IPublishedContent content, #region Axes: custom /// - /// Gets the root content for this content. + /// Gets the root content (ancestor or self at level 1) for the specified . /// /// The content. - /// The 'site' content ie AncestorOrSelf(1). + /// + /// The root content (ancestor or self at level 1) for the specified . + /// + /// + /// This is the same as calling with maxLevel set to 1. + /// public static IPublishedContent Root(this IPublishedContent content) { return content.AncestorOrSelf(1); } + /// + /// Gets the root content (ancestor or self at level 1) for the specified if it's of the specified content type . + /// + /// The content type. + /// The content. + /// + /// The root content (ancestor or self at level 1) for the specified of content type . + /// + /// + /// This is the same as calling with maxLevel set to 1. + /// + public static T Root(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.AncestorOrSelf(1); + } + #endregion #region PropertyAliasesAndNames diff --git a/src/Umbraco.Web/PublishedPropertyExtension.cs b/src/Umbraco.Web/PublishedPropertyExtension.cs index 0c3aa57cc259..6e8647db475a 100644 --- a/src/Umbraco.Web/PublishedPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedPropertyExtension.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; @@ -37,13 +36,20 @@ public static T Value(this IPublishedProperty property, string culture = null // we have a value // try to cast or convert it var value = property.GetValue(culture, segment); - if (value is T valueAsT) return valueAsT; + if (value is T valueAsT) + { + return valueAsT; + } + var valueConverted = value.TryConvertTo(); - if (valueConverted) return valueConverted.Result; + if (valueConverted) + { + return valueConverted.Result; + } - // cannot cast nor convert the value, nothing we can return but 'defaultValue' + // cannot cast nor convert the value, nothing we can return but 'default' // note: we don't want to fallback in that case - would make little sense - return defaultValue; + return default; } // we don't have a value, try fallback @@ -57,15 +63,22 @@ public static T Value(this IPublishedProperty property, string culture = null var noValue = property.GetValue(culture, segment); if (noValue == null) { - return defaultValue; + return default; + } + + if (noValue is T noValueAsT) + { + return noValueAsT; } - if (noValue is T noValueAsT) return noValueAsT; var noValueConverted = noValue.TryConvertTo(); - if (noValueConverted) return noValueConverted.Result; + if (noValueConverted) + { + return noValueConverted.Result; + } - // cannot cast noValue nor convert it, nothing we can return but 'defaultValue' - return defaultValue; + // cannot cast noValue nor convert it, nothing we can return but 'default' + return default; } #endregion diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index a02fd5872a9c..ebf935dcf8e6 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -427,7 +427,7 @@ internal void FindPublishedContent(PublishedRequest request) return finder.TryFindContent(request); }); - _profilingLogger.Debug( + _logger.Debug( "Found? {Found} Content: {PublishedContentId}, Template: {TemplateAlias}, Domain: {Domain}, Culture: {Culture}, Is404: {Is404}, StatusCode: {StatusCode}", found, request.HasPublishedContent ? request.PublishedContent.Id : "NULL", @@ -516,55 +516,47 @@ private bool FollowInternalRedirects(PublishedRequest request) // don't try to find a redirect if the property doesn't exist if (request.PublishedContent.HasProperty(Constants.Conventions.Content.InternalRedirectId) == false) + { return false; + } - var redirect = false; - var valid = false; - IPublishedContent internalRedirectNode = null; - var internalRedirectId = request.PublishedContent.Value(Constants.Conventions.Content.InternalRedirectId, defaultValue: -1); + var internalRedirectId = request.PublishedContent.Value(Constants.Conventions.Content.InternalRedirectId)?.ToString(); - if (internalRedirectId > 0) + if (internalRedirectId == null) { - // try and get the redirect node from a legacy integer ID - valid = true; - internalRedirectNode = request.UmbracoContext.Content.GetById(internalRedirectId); + // no value stored, just return, no need to log + return false; } - else + + if (int.TryParse(internalRedirectId, out var internalRedirectIdAsInt) && internalRedirectIdAsInt == request.PublishedContent.Id) { - var udiInternalRedirectId = request.PublishedContent.Value(Constants.Conventions.Content.InternalRedirectId); - if (udiInternalRedirectId != null) - { - // try and get the redirect node from a UDI Guid - valid = true; - internalRedirectNode = request.UmbracoContext.Content.GetById(udiInternalRedirectId.Guid); - } + // redirect to self + _logger.Debug("FollowInternalRedirects: Redirecting to self, ignore"); + return false; } - if (valid == false) + IPublishedContent internalRedirectNode = null; + if (internalRedirectIdAsInt > 0) { - // bad redirect - log and display the current page (legacy behavior) - _logger.Debug("FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: value is not an int nor a GuidUdi.", - request.PublishedContent.GetProperty(Constants.Conventions.Content.InternalRedirectId).GetSourceValue()); + // try and get the redirect node from a legacy integer ID + internalRedirectNode = request.UmbracoContext.Content.GetById(internalRedirectIdAsInt); + } + else if (GuidUdi.TryParse(internalRedirectId, out var internalRedirectIdAsUdi)) + { + // try and get the redirect node from a UDI Guid + internalRedirectNode = request.UmbracoContext.Content.GetById(internalRedirectIdAsUdi.Guid); } if (internalRedirectNode == null) { _logger.Debug("FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: no such published document.", request.PublishedContent.GetProperty(Constants.Conventions.Content.InternalRedirectId).GetSourceValue()); - } - else if (internalRedirectId == request.PublishedContent.Id) - { - // redirect to self - _logger.Debug("FollowInternalRedirects: Redirecting to self, ignore"); - } - else - { - request.SetInternalRedirectPublishedContent(internalRedirectNode); // don't use .PublishedContent here - redirect = true; - _logger.Debug("FollowInternalRedirects: Redirecting to id={InternalRedirectId}", internalRedirectId); + return false; } - return redirect; + request.SetInternalRedirectPublishedContent(internalRedirectNode); // don't use .PublishedContent here + _logger.Debug("FollowInternalRedirects: Redirecting to id={InternalRedirectId}", internalRedirectIdAsInt); + return true; } /// diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index b15641b503ce..5d97bfe4a2d0 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -40,7 +40,6 @@ using Umbraco.Web.PropertyEditors; using Umbraco.Core.Models; using Umbraco.Web.Models; -using Ganss.XSS; namespace Umbraco.Web.Runtime { @@ -140,15 +139,6 @@ public override void Compose(Composition composition) composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); - composition.Register(_ => - { - var sanitizer = new HtmlSanitizer(); - sanitizer.AllowedAttributes.UnionWith(Umbraco.Core.Constants.SvgSanitizer.Attributes); - sanitizer.AllowedCssProperties.UnionWith(Umbraco.Core.Constants.SvgSanitizer.Attributes); - sanitizer.AllowedTags.UnionWith(Umbraco.Core.Constants.SvgSanitizer.Tags); - return sanitizer; - },Lifetime.Singleton); - composition.RegisterUnique(factory => ExamineManager.Instance); // configure the container for web diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index a126592ffcff..e8cb59253604 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -319,7 +319,10 @@ private void StartUpLocked() // create a new token source since this is a new process _shutdownTokenSource = new CancellationTokenSource(); _shutdownToken = _shutdownTokenSource.Token; - _runningTask = Task.Run(async () => await Pump().ConfigureAwait(false), _shutdownToken); + using (ExecutionContext.SuppressFlow()) + { + _runningTask = Task.Run(async () => await Pump().ConfigureAwait(false), _shutdownToken); + } _logger.Debug("{LogPrefix} Starting", _logPrefix); } @@ -544,10 +547,14 @@ private async Task RunAsync(T bgTask, CancellationToken token) try { if (bgTask.IsAsync) + { // configure await = false since we don't care about the context, we're on a background thread. await bgTask.RunAsync(token).ConfigureAwait(false); + } else + { bgTask.Run(); + } } finally // ensure we disposed - unless latched again ie wants to re-run { @@ -710,14 +717,20 @@ internal Task StopInternal(bool immediate) // with a single aspnet thread during shutdown and we don't want to delay other calls to IRegisteredObject.Stop. if (!immediate) { - return Task.Run(StopInitial, CancellationToken.None); + using (ExecutionContext.SuppressFlow()) + { + return Task.Run(StopInitial, CancellationToken.None); + } } else { lock (_locker) { if (_terminated) return Task.CompletedTask; - return Task.Run(StopImmediate, CancellationToken.None); + using (ExecutionContext.SuppressFlow()) + { + return Task.Run(StopImmediate, CancellationToken.None); + } } } } diff --git a/src/Umbraco.Web/Services/IconService.cs b/src/Umbraco.Web/Services/IconService.cs index 15e673e6bae7..fad53103c0af 100644 --- a/src/Umbraco.Web/Services/IconService.cs +++ b/src/Umbraco.Web/Services/IconService.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using Ganss.XSS; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; @@ -15,13 +14,11 @@ namespace Umbraco.Web.Services public class IconService : IIconService { private readonly IGlobalSettings _globalSettings; - private readonly IHtmlSanitizer _htmlSanitizer; private readonly IAppPolicyCache _cache; - public IconService(IGlobalSettings globalSettings, IHtmlSanitizer htmlSanitizer, AppCaches appCaches) + public IconService(IGlobalSettings globalSettings, AppCaches appCaches) { _globalSettings = globalSettings; - _htmlSanitizer = htmlSanitizer; _cache = appCaches.RuntimeCache; } @@ -78,12 +75,11 @@ private IconModel CreateIconModel(string iconName, string iconPath) try { var svgContent = System.IO.File.ReadAllText(iconPath); - var sanitizedString = _htmlSanitizer.Sanitize(svgContent); var svg = new IconModel { Name = iconName, - SvgString = sanitizedString + SvgString = svgContent }; return svg; diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index c0a9d15cfaa0..37496d1bff89 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -129,10 +129,12 @@ protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection if (_isUmbracoProvider) { - nodes.AddRange(Services.MemberTypeService.GetAll() - .Select(memberType => - CreateTreeNode(memberType.Alias, id, queryStrings, memberType.Name, memberType.Icon.IfNullOrWhiteSpace(Constants.Icons.Member), true, - queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/list/" + memberType.Alias))); + nodes.AddRange( + Services.MemberTypeService.GetAll() + .OrderBy(x => x.Name) + .Select(memberType => + CreateTreeNode(memberType.Alias, id, queryStrings, memberType.Name, memberType.Icon.IfNullOrWhiteSpace(Constants.Icons.Member), true, + queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/list/" + memberType.Alias))); } } diff --git a/src/Umbraco.Web/Trees/MemberTypeAndGroupTreeControllerBase.cs b/src/Umbraco.Web/Trees/MemberTypeAndGroupTreeControllerBase.cs index 5e71266bcabb..61b9b3e06337 100644 --- a/src/Umbraco.Web/Trees/MemberTypeAndGroupTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/MemberTypeAndGroupTreeControllerBase.cs @@ -13,6 +13,10 @@ public abstract class MemberTypeAndGroupTreeControllerBase : TreeController protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { var nodes = new TreeNodeCollection(); + + // if the request is for folders only then just return + if (queryStrings["foldersonly"].IsNullOrWhiteSpace() == false && queryStrings["foldersonly"] == "1") return nodes; + nodes.AddRange(GetTreeNodesFromService(id, queryStrings)); return nodes; } @@ -30,7 +34,13 @@ protected override MenuItemCollection GetMenuForNode(string id, FormDataCollecti } else { - //delete member type/group + var memberType = Services.MemberTypeService.Get(int.Parse(id)); + if (memberType != null) + { + menu.Items.Add(Services.TextService, opensDialog: true); + } + + // delete member type/group menu.Items.Add(Services.TextService, opensDialog: true); } diff --git a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs index 5db9088f20da..85f61d5fed62 100644 --- a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs @@ -29,6 +29,7 @@ protected override TreeNode CreateRootNode(FormDataCollection queryStrings) root.HasChildren = Services.MemberTypeService.GetAll().Any(); return root; } + protected override IEnumerable GetTreeNodesFromService(string id, FormDataCollection queryStrings) { return Services.MemberTypeService.GetAll() diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a6096692b408..a0e074c9c12e 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -65,9 +65,6 @@ - - 5.0.376 - 2.7.0.100