diff --git a/Directory.Packages.props b/Directory.Packages.props
index ccd1e91a2e..aff37a4b02 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,30 +5,30 @@
-
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
+
+
@@ -45,13 +45,13 @@
-
-
-
+
+
+
-
+
-
+
@@ -62,22 +62,22 @@
-
+
-
+
-
-
+
+
-
+
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs
index a9c670b7ad..102bf16224 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs
@@ -160,11 +160,12 @@ public class MemberController : DeliveryApiControllerBase
claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken);
}
- if (request.GetScopes().Contains(OpenIddictConstants.Scopes.OfflineAccess))
- {
- // "offline_access" scope is required to use refresh tokens
- memberPrincipal.SetScopes(OpenIddictConstants.Scopes.OfflineAccess);
- }
+ // "openid" and "offline_access" are the only scopes allowed for members; explicitly ensure we only add those
+ // NOTE: the "offline_access" scope is required to use refresh tokens
+ IEnumerable allowedScopes = request
+ .GetScopes()
+ .Intersect(new[] { OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess });
+ memberPrincipal.SetScopes(allowedScopes);
return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, memberPrincipal);
}
diff --git a/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj b/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj
index 724c5b5e34..c513082675 100644
--- a/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj
+++ b/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs
index a35f7aa956..c6f21738c2 100644
--- a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs
@@ -129,7 +129,7 @@ public abstract class RecurringHostedServiceBase : IHostedService, IDisposable
/// Executes the task.
///
/// The task state.
- public async void ExecuteAsync(object? state)
+ public virtual async void ExecuteAsync(object? state)
{
try
{
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js
index ab6c176c85..85ca297e69 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js
@@ -401,8 +401,9 @@
// If we are in the document type editor, we need to use -20 as the current page id.
// If we are in the content editor, we need to use the current page id or parent id if the current page is new.
// We can recognize a content editor context by checking if the current editor state has a contentTypeKey.
+ // If no current is represented, we will use null.
const currentEditorState = editorState.getCurrent();
- const currentPageId = currentEditorState.contentTypeKey ? currentEditorState.id || currentEditorState.parentId || -20 : -20;
+ const currentPageId = currentEditorState ? currentEditorState.contentTypeKey ? currentEditorState.id || currentEditorState.parentId || -20 : -20 : null;
// Load all scaffolds for the block types.
// The currentPageId is used to determine the access level for the current user.
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js
index ebf759c20c..df5b0d51bb 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js
@@ -1457,12 +1457,13 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
function syncContent() {
- const content = args.editor.getContent()
+ const content = args.editor.getContent();
if (getPropertyValue() === content) {
return;
}
+
//stop watching before we update the value
stopWatch();
angularHelper.safeApply($rootScope, function () {
@@ -1493,8 +1494,9 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
const blockEls = args.editor.contentDocument.querySelectorAll('umb-rte-block, umb-rte-block-inline');
for (var blockEl of blockEls) {
if(!blockEl._isInitializedUmbBlock) {
+ // First time we initialize this block element
const blockContentUdi = blockEl.getAttribute('data-content-udi');
- if(blockContentUdi && !blockEl.$block) {
+ if(blockContentUdi) {
const block = args.blockEditorApi.getBlockByContentUdi(blockContentUdi);
if(block) {
blockEl.removeAttribute('contenteditable');
@@ -1533,9 +1535,23 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
} else {
args.editor.dom.remove(blockEl);
}
+ } else {
+ // Second time we initialize this block element, cause by a new Block Model Object has been initiated. (Mainly cause we re initiate all blocks when there is a value update)
+ const blockContentUdi = blockEl.getAttribute('data-content-udi');
+ const block = args.blockEditorApi.getBlockByContentUdi(blockContentUdi);
+ if(block) {
+ blockEl.$index = block.index;
+ blockEl.$block = block;
+ blockEl.update();
+ } else {
+ console.error('Could not find block with content udi: ' + blockContentUdi);
+ }
}
}
}
+ args.editor.on('updateBlocks', function () {
+ initBlocks();
+ });
// If we can not find the insert image/media toolbar button
// Then we need to add an event listener to the editor
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/umb-rte-block-clipboard.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/umb-rte-block-clipboard.component.js
new file mode 100644
index 0000000000..216b7fe1e8
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/umb-rte-block-clipboard.component.js
@@ -0,0 +1,95 @@
+/**
+ * @ngdoc service
+ * @name umbraco.services.rte-block-clipboard-service
+ *
+ * Handles clipboard resolvers for Block of RTE properties.
+ *
+ */
+(function () {
+ 'use strict';
+
+
+
+ /**
+ * When performing a runtime copy of Block Editors entries, we copy the ElementType Data Model and inner IDs are kept identical, to ensure new IDs are changed on paste we need to provide a resolver for the ClipboardService.
+ */
+ angular.module('umbraco').run(['clipboardService', 'udiService', function (clipboardService, udiService) {
+
+ function replaceUdi(obj, key, dataObject, markup) {
+ var udi = obj[key];
+ var newUdi = udiService.create("element");
+ obj[key] = newUdi;
+ dataObject.forEach((data) => {
+ if (data.udi === udi) {
+ data.udi = newUdi;
+ }
+ });
+ // make a attribute name of the key, by kebab casing it:
+ var attrName = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
+ // replace the udi in the markup as well.
+ var regex = new RegExp('data-'+attrName+'="'+udi+'"', "g");
+ markup = markup.replace(regex, 'data-'+attrName+'="'+newUdi+'"');
+ return markup;
+ }
+ function replaceUdisOfObject(obj, propValue, markup) {
+ for (var k in obj) {
+ if(k === "contentUdi") {
+ markup = replaceUdi(obj, k, propValue.contentData, markup);
+ } else if(k === "settingsUdi") {
+ markup = replaceUdi(obj, k, propValue.settingsData, markup);
+ } else {
+ // lets crawl through all properties of layout to make sure get captured all `contentUdi` and `settingsUdi` properties.
+ var propType = typeof obj[k];
+ if(propType != null && (propType === "object" || propType === "array")) {
+ markup = replaceUdisOfObject(obj[k], propValue, markup);
+ }
+ }
+ }
+ return markup
+ }
+
+
+ function rawRteBlockResolver(propertyValue, propPasteResolverMethod) {
+ if (propertyValue != null && typeof propertyValue === "object") {
+
+ // object property of 'blocks' holds the data for the Block Editor.
+ var value = propertyValue.blocks;
+
+ // we got an object, and it has these three props then we are most likely dealing with a Block Editor.
+ if ((value.layout !== undefined && value.contentData !== undefined && value.settingsData !== undefined)) {
+
+ // replaceUdisOfObject replaces udis of the value object(by instance reference), but also returns the updated markup (as we cant update the reference of a string).
+ propertyValue.markup = replaceUdisOfObject(value.layout, value, propertyValue.markup);
+
+ // run resolvers for inner properties of this Blocks content data.
+ if(value.contentData.length > 0) {
+ value.contentData.forEach((item) => {
+ for (var k in item) {
+ propPasteResolverMethod(item[k], clipboardService.TYPES.RAW);
+ }
+ });
+ }
+ // run resolvers for inner properties of this Blocks settings data.
+ if(value.settingsData.length > 0) {
+ value.settingsData.forEach((item) => {
+ for (var k in item) {
+ propPasteResolverMethod(item[k], clipboardService.TYPES.RAW);
+ }
+ });
+ }
+
+ }
+ }
+ }
+
+ function elementTypeBlockResolver(obj, propPasteResolverMethod) {
+ // we could filter for specific Property Editor Aliases, but as the Block Editor structure can be used by many Property Editor we do not in this code know a good way to detect that this is a Block Editor and will therefor leave it to the value structure to determin this.
+ rawRteBlockResolver(obj.value, propPasteResolverMethod);
+ }
+
+ clipboardService.registerPastePropertyResolver(elementTypeBlockResolver, clipboardService.TYPES.ELEMENT_TYPE);
+ clipboardService.registerPastePropertyResolver(rawRteBlockResolver, clipboardService.TYPES.RAW);
+
+ }]);
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/umb-rte-block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/umb-rte-block.component.js
index 0c4c8e2e50..7738a73c9f 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/umb-rte-block.component.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/umb-rte-block.component.js
@@ -18,7 +18,8 @@
}
function onInit() {
- $element[0]._isInitializedUmbBlock = true;
+
+ // We might call onInit multiple times(via the update method), so parsing the latest values is needed no matter if it has been initialized before.
$scope.block = $element[0].$block;
$scope.api = $element[0].$api;
$scope.index = $element[0].$index;
@@ -27,6 +28,13 @@
$scope.parentForm = $element[0].$parentForm;
$scope.valFormManager = $element[0].$valFormManager;
+ if($element[0]._isInitializedUmbBlock === true) {
+ return;
+ }
+ $element[0]._isInitializedUmbBlock = true;
+ $element[0].update = onInit;
+
+
const stylesheet = $scope.block.config.stylesheet;
var shadowRoot = $element[0].attachShadow({ mode: 'open' });
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js
index 7f06215148..d499f47a9c 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js
@@ -264,7 +264,7 @@
},
culture: vm.umbProperty?.culture ?? null,
segment: vm.umbProperty?.segment ?? null,
- blockEditorApi: vm.noBlocksMode ? undefined : vm.blockEditorApi,
+ blockEditorApi: vm.noBlocksMode ? undefined : vm.blockEditorApi,
parentForm: vm.propertyForm,
valFormManager: vm.valFormManager,
currentFormInput: $scope.rteForm.modelValue
@@ -341,10 +341,14 @@
// we need to deal with that here so that our model values are all in sync so we basically re-initialize.
function onServerValueChanged(newVal, oldVal) {
+
ensurePropertyValue(newVal);
+ // updating the modelObject with the new value cause a angular compile issue.
+ // But I'm not sure it's needed, as this does not trigger the RTE
if(modelObject) {
modelObject.update(vm.model.value.blocks, $scope);
+ vm.tinyMceEditor.fire('updateBlocks');
}
onLoaded();
}
@@ -944,7 +948,19 @@
return undefined;
}
- return vm.layout[layoutIndex].$block;
+ var layoutEntry = vm.layout[layoutIndex];
+ if(layoutEntry.$block === undefined || layoutEntry.$block.config === undefined) {
+ // make block model
+ var blockObject = getBlockObject(layoutEntry);
+ if (blockObject === null) {
+ // Initialization of the Block Object didn't go well, therefor we will fail the paste action.
+ return false;
+ }
+
+ // set the BlockObject on our layout entry.
+ layoutEntry.$block = blockObject;
+ }
+ return layoutEntry.$block;
}
vm.blockEditorApi = {