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
Creative 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.
]]>
Umbraco Development
@@ -218,7 +218,7 @@ Google Maps - A map macro that you can use within Rich Text Areas
Contact Us
-
We'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
]]>
+
We'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.
]]>
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
Creative 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.
Umbraco 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
-
We'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
]]>
+
We'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
@@ -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.
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.
",
"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": "
",
"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.
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.
-
-
-
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