diff --git a/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs b/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs index bb5c186ca6..2a01d42dc7 100644 --- a/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs +++ b/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs @@ -8,13 +8,23 @@ namespace Umbraco.Cms.Core.Xml; /// public class UmbracoXPathPathSyntaxParser { + [Obsolete("This will be removed in Umbraco 13. Use ParseXPathQuery which accepts a parentId instead")] + public static string ParseXPathQuery( + string xpathExpression, + int? nodeContextId, + Func?> getPath, + Func publishedContentExists) => ParseXPathQuery(xpathExpression, nodeContextId, null, getPath, publishedContentExists); + /// /// Parses custom umbraco xpath expression /// /// The Xpath expression /// - /// The current node id context of executing the query - null if there is no current node, in which case - /// some of the parameters like $current, $parent, $site will be disabled + /// The current node id context of executing the query - null if there is no current node. + /// + /// + /// The parent node id of the current node id context of executing the query. With this we can determine the + /// $parent and $site parameters even if the current node is not yet published. /// /// The callback to create the nodeId path, given a node Id /// The callback to return whether a published node exists based on Id @@ -22,6 +32,7 @@ public class UmbracoXPathPathSyntaxParser public static string ParseXPathQuery( string xpathExpression, int? nodeContextId, + int? parentId, Func?> getPath, Func publishedContentExists) { @@ -84,19 +95,27 @@ public class UmbracoXPathPathSyntaxParser // parseable items: var vars = new Dictionary>(); - // These parameters must have a node id context - if (nodeContextId.HasValue) + if (parentId.HasValue) + { + vars.Add("$parent", q => + { + var path = getPath(parentId.Value)?.ToArray(); + var closestPublishedAncestorId = getClosestPublishedAncestor(path); + return q.Replace("$parent", string.Format(rootXpath, closestPublishedAncestorId)); + }); + + vars.Add("$site", q => + { + var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(parentId.Value)); + return q.Replace( + "$site", + string.Format(rootXpath, closestPublishedAncestorId) + "/ancestor-or-self::*[@level = 1]"); + }); + } + else if (nodeContextId.HasValue) { - vars.Add("$current", q => - { - var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); - return q.Replace("$current", string.Format(rootXpath, closestPublishedAncestorId)); - }); - vars.Add("$parent", q => { - // remove the first item in the array if its the current node - // this happens when current is published, but we are looking for its parent specifically var path = getPath(nodeContextId.Value)?.ToArray(); if (path?[0] == nodeContextId.ToString()) { @@ -116,6 +135,16 @@ public class UmbracoXPathPathSyntaxParser }); } + // These parameters must have a node id context + if (nodeContextId.HasValue) + { + vars.Add("$current", q => + { + var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); + return q.Replace("$current", string.Format(rootXpath, closestPublishedAncestorId)); + }); + } + // TODO: This used to just replace $root with string.Empty BUT, that would never work // the root is always "/root . Need to confirm with Per why this was string.Empty before! vars.Add("$root", q => q.Replace("$root", "/root")); diff --git a/src/Umbraco.Infrastructure/Routing/NotFoundHandlerHelper.cs b/src/Umbraco.Infrastructure/Routing/NotFoundHandlerHelper.cs index f945f03b28..04ca6b4bb2 100644 --- a/src/Umbraco.Infrastructure/Routing/NotFoundHandlerHelper.cs +++ b/src/Umbraco.Infrastructure/Routing/NotFoundHandlerHelper.cs @@ -79,6 +79,7 @@ internal class NotFoundHandlerHelper var xpathResult = UmbracoXPathPathSyntaxParser.ParseXPathQuery( errorPage.ContentXPath!, domainContentId, + null, nodeid => { IEntitySlim? ent = entityService.Get(nodeid); diff --git a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs index fe88701266..750d60d67e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs @@ -515,6 +515,15 @@ public class EntityController : UmbracoAuthorizedJsonController return Ok(returnUrl); } + /// + /// Gets an entity by a xpath query - OBSOLETE + /// + /// + /// + /// + /// + [Obsolete("This will be removed in Umbraco 13. Use GetByXPath instead")] + public ActionResult? GetByQuery(string query, int nodeContextId, UmbracoEntityTypes type) => GetByXPath(query, nodeContextId, null, type); /// /// Gets an entity by a xpath query @@ -522,19 +531,16 @@ public class EntityController : UmbracoAuthorizedJsonController /// /// /// + /// /// - public ActionResult? GetByQuery(string query, int nodeContextId, UmbracoEntityTypes type) + public ActionResult? GetByXPath(string query, int nodeContextId, int? parentId, UmbracoEntityTypes type) { - // TODO: Rename this!!! It's misleading, it should be GetByXPath - - if (type != UmbracoEntityTypes.Document) { throw new ArgumentException("Get by query is only compatible with entities of type Document"); } - - var q = ParseXPathQuery(query, nodeContextId); + var q = ParseXPathQuery(query, nodeContextId, parentId); IPublishedContent? node = _publishedContentQuery.ContentSingleAtXPath(q); if (node == null) @@ -546,10 +552,11 @@ public class EntityController : UmbracoAuthorizedJsonController } // PP: Work in progress on the query parser - private string ParseXPathQuery(string query, int id) => + private string ParseXPathQuery(string query, int id, int? parentId) => UmbracoXPathPathSyntaxParser.ParseXPathQuery( query, id, + parentId, nodeid => { IEntitySlim? ent = _entityService.Get(nodeid); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index a3c96cf805..6f46268b5b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -318,9 +318,22 @@ function entityResource($q, $http, umbRequestHelper) { 'Failed to retrieve entity data for ids ' + ids); }, + /** + * @deprecated use getByXPath instead. + */ + getByQuery: function (query, nodeContextId, type) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "entityApiBaseUrl", + "GetByQuery", + [{ query: query }, { nodeContextId: nodeContextId }, { type: type }])), + 'Failed to retrieve entity data for query ' + query); + }, + /** * @ngdoc method - * @name umbraco.resources.entityResource#getByQuery + * @name umbraco.resources.entityResource#getByXPath * @methodOf umbraco.resources.entityResource * * @description @@ -329,7 +342,7 @@ function entityResource($q, $http, umbRequestHelper) { * ##usage *
          * //get content by xpath
-         * entityResource.getByQuery("$current", -1, "Document")
+         * entityResource.getByXPath("$current", -1, -1, "Document")
          *    .then(function(ent) {
          *        var myDoc = ent;
          *        alert('its here!');
@@ -338,17 +351,18 @@ function entityResource($q, $http, umbRequestHelper) {
          *
          * @param {string} query xpath to use in query
          * @param {Int} nodeContextId id id to start from
+         * @param {Int} parentId id id of the parent to the starting point
          * @param {string} type Object type name
          * @returns {Promise} resourcePromise object containing the entity.
          *
          */
-        getByQuery: function (query, nodeContextId, type) {
+        getByXPath: function (query, nodeContextId, parentId, type) {
             return umbRequestHelper.resourcePromise(
                $http.get(
                    umbRequestHelper.getApiUrl(
                        "entityApiBaseUrl",
-                       "GetByQuery",
-                       [{ query: query }, { nodeContextId: nodeContextId }, { type: type }])),
+                       "GetByXPath",
+                       [{ query: query }, { nodeContextId: nodeContextId }, { parentId: parentId }, { type: type }])),
                'Failed to retrieve entity data for query ' + query);
         },
 
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js
index 09b9d2c98b..52f147bce0 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js
@@ -245,9 +245,12 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso
         dialogOptions.startNodeId = -1;
     }
     else if ($scope.model.config.startNode.query) {
-        //if we have a query for the startnode, we will use that.
-        var rootId = editorState.current.id;
-        entityResource.getByQuery($scope.model.config.startNode.query, rootId, "Document").then(function (ent) {
+        entityResource.getByXPath(
+            $scope.model.config.startNode.query,
+            editorState.current.id,
+            editorState.current.parentId,
+            "Document"
+        ).then(function (ent) {
             dialogOptions.startNodeId = ($scope.model.config.idType === "udi" ? ent.udi : ent.id).toString();
         });
     }
diff --git a/templates/UmbracoProject/UmbracoProject.csproj b/templates/UmbracoProject/UmbracoProject.csproj
index 0989706f68..d50f95a907 100644
--- a/templates/UmbracoProject/UmbracoProject.csproj
+++ b/templates/UmbracoProject/UmbracoProject.csproj
@@ -26,8 +26,6 @@
     false
     false
   
-
-