diff --git a/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml b/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml
index eda0baeffada53eb5f6cee94f873084677cf73ae..543cf4589f40b1ade6a6dc7b5bc1f2ee854bd0a7 100644
--- a/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml
+++ b/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml
@@ -881,33 +881,6 @@
 #end
 
 
-#macro (parseDateRange $matchType $filterValue $dateRange)
-  ## Transform the filter value into a date range if needed.
-  #if ($matchType == 'after')
-    #set ($dateRangeString = "$filterValue/")
-  #elseif ($matchType == 'before')
-    #set ($dateRangeString = "/$filterValue")
-  #else
-    ## Between start and end date.
-    #set ($dateRangeString = $filterValue)
-  #end
-  ## Try to parse as ISO 8601 time interval (see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals)
-  #set ($parts = $dateRangeString.split('/', -1))
-  #if ($parts.size() == 2)
-    #set ($dateRange.start = $datetool.toDate('iso_tz', $parts[0]))
-    #set ($dateRange.end = $datetool.toDate('iso_tz', $parts[1]))
-  #end
-  #if (!$dateRange.start && !$dateRange.end)
-    ## Try to parse as timestamp range. Note that this doesn't handle well negative timestamps.
-    #set ($parts = $dateRangeString.split('-', -1))
-    #if ($parts.size() == 2)
-      #set ($dateRange.start = $datetool.toDate($numbertool.toNumber($parts[0])))
-      #set ($dateRange.end = $datetool.toDate($numbertool.toNumber($parts[1])))
-    #end
-  #end
-#end
-
-
 #**
  * NOTE: This macro uses variables defined in livetable_filterProperty . It was not meant to be used alone.
  *#
diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/getdocuments.vm b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/getdocuments.vm
index e7a3f7298ec1681b593201eec11c972a388f950e..5db162dc437f3d6d0fab2c852ec2c82c9842e951 100644
--- a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/getdocuments.vm
+++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/getdocuments.vm
@@ -56,13 +56,20 @@ $response.setContentType("application/json")
 ##
 #set ($dateFilter = $request.get("doc.date"))
 #if("$!{dateFilter}" != '')
-  #set ($dates = $dateFilter.split('-'))
-  #if ($dates.size() == 2)
+  #set ($dateRange = {})
+  #parseDateRange($matchType $dateFilter $dateRange)
+  #if ($dateRange.start && $dateRange.end)
     ## Date range matching
-    #set ($discard = $queryParams.add($datetool.toDate($numbertool.toNumber($dates[0]))))
+    #set ($discard = $queryParams.add($dateRange.start))
     #set ($whereQueryPart = "${whereQueryPart} and doc.date between ?$queryParams.size()")
-    #set ($discard = $queryParams.add($datetool.toDate($numbertool.toNumber($dates[1]))))
+    #set ($discard = $queryParams.add($dateRange.end))
     #set ($whereQueryPart = "${whereQueryPart} and ?$queryParams.size()")
+  #elseif ($dateRange.start)
+    #set ($discard = $queryParams.add($dateRange.start))
+    #set ($whereQueryPart = "${whereQueryPart} and doc.date >= ?$queryParams.size()")
+  #elseif ($dateRange.end)
+    #set ($discard = $queryParams.add($dateRange.end))
+    #set ($whereQueryPart = "${whereQueryPart} and doc.date <= ?$queryParams.size()")
   #else
     ## Single value matching
     #set ($discard = $queryParams.add($dateFilter))
diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/macros.vm b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/macros.vm
index ff79bd64e9097c7dc886e7df996871e2943bca20..ceaa65b4d3651125356580801120d6b3e789aaf9 100644
--- a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/macros.vm
+++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/macros.vm
@@ -2937,3 +2937,42 @@ Recursive title display detected!##
     <a href="$xwiki.getURL($docReference)">$escapetool.xml($docReference.name)</a>
   </div>
 #end
+
+##
+## Parse the provided filterValue according to its match type and assign the resulting start/end dates to the dateRange
+## map.
+## First, if a after or before matchType is provided, a '/' is added respectivelly at the end or at the beguinning of  
+## filterValue.
+## Then, we first start by trying to split filterValue using a '/', and parse the two substring as ISO 8601 dates.
+## If none of the substring conforms to the ISO 8601 date format, a second try is done by splitting using '-', and 
+## the two substrings are parsed as timestamps.
+##
+## @since 14.0RC1
+## @since 13.10.1
+## @since 13.4.6
+##
+#macro (parseDateRange $matchType $filterValue $dateRange)
+  ## Transform the filter value into a date range if needed.
+  #if ($matchType == 'after')
+    #set ($dateRangeString = "$filterValue/")
+  #elseif ($matchType == 'before')
+    #set ($dateRangeString = "/$filterValue")
+  #else
+    ## Between start and end date.
+    #set ($dateRangeString = $filterValue)
+  #end
+  ## Try to parse as ISO 8601 time interval (see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals)
+  #set ($parts = $dateRangeString.split('/', -1))
+  #if ($parts.size() == 2)
+    #set ($dateRange.start = $datetool.toDate('iso_tz', $parts[0]))
+    #set ($dateRange.end = $datetool.toDate('iso_tz', $parts[1]))
+  #end
+  #if (!$dateRange.start && !$dateRange.end)
+    ## Try to parse as timestamp range. Note that this doesn't handle well negative timestamps.
+    #set ($parts = $dateRangeString.split('-', -1))
+    #if ($parts.size() == 2)
+      #set ($dateRange.start = $datetool.toDate($numbertool.toNumber($parts[0])))
+      #set ($dateRange.end = $datetool.toDate($numbertool.toNumber($parts[1])))
+    #end
+  #end
+#end
diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/test/java/org/xwiki/web/GetdocumentsPageTest.java b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/test/java/org/xwiki/web/GetdocumentsPageTest.java
index fd8ecee2a0038f3be917f1cfff12e2ee4737ee29..0456d1dffcefe44714d717e761ee173da068c513 100644
--- a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/test/java/org/xwiki/web/GetdocumentsPageTest.java
+++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/test/java/org/xwiki/web/GetdocumentsPageTest.java
@@ -30,18 +30,21 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.xwiki.model.script.ModelScriptService;
+import org.xwiki.query.QueryException;
 import org.xwiki.query.internal.ScriptQuery;
 import org.xwiki.query.script.QueryManagerScriptService;
 import org.xwiki.script.service.ScriptService;
 import org.xwiki.template.TemplateManager;
 import org.xwiki.test.annotation.ComponentList;
 import org.xwiki.test.page.PageTest;
+import org.xwiki.velocity.VelocityManager;
 import org.xwiki.velocity.internal.XWikiDateTool;
 import org.xwiki.velocity.tools.EscapeTool;
 import org.xwiki.velocity.tools.JSONTool;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -74,16 +77,22 @@ class GetdocumentsPageTest extends PageTest
 
     private TemplateManager templateManager;
 
+    private VelocityManager velocityManager;
+
+    private JSONTool jsonTool;
+
     @BeforeEach
     void setUp() throws Exception
     {
         this.templateManager = this.oldcore.getMocker().getInstance(TemplateManager.class);
+        this.velocityManager = this.oldcore.getMocker().getInstance(VelocityManager.class);
         this.oldcore.getMocker().registerComponent(ScriptService.class, "query", this.queryService);
 
         registerVelocityTool("jsontool", new JSONTool());
         registerVelocityTool("mathtool", new MathTool());
         registerVelocityTool("escapetool", new EscapeTool());
         registerVelocityTool("numbertool", new NumberTool());
+        registerVelocityTool("datetool", this.componentManager.getInstance(XWikiDateTool.class));
     }
 
     @Test
@@ -92,12 +101,7 @@ void removeObuscatedResultsWhenTotalrowsLowerThanLimit() throws Exception
         when(this.oldcore.getMockRightService().hasAccessLevel(eq("view"), any(), any(), any()))
             .thenReturn(false, true);
         this.request.put("limit", "2");
-        when(this.queryService.hql(anyString())).thenReturn(this.query);
-        when(this.query.setLimit(anyInt())).thenReturn(this.query);
-        when(this.query.setOffset(anyInt())).thenReturn(this.query);
-        when(this.query.bindValues(any(Map.class))).thenReturn(this.query);
-        when(this.query.bindValues(any(List.class))).thenReturn(this.query);
-        when(this.query.count()).thenReturn(3L);
+        initDefaultQueryMocks(3);
         when(this.query.execute()).thenReturn(Arrays.asList("XWiki.NotViewable", "XWiki.Viewable"));
 
         Map<String, Object> result = getJsonResultMap();
@@ -120,12 +124,7 @@ void nonViewableResultsAreObfuscated() throws Exception
         when(this.oldcore.getMockRightService().hasAccessLevel(eq("view"), any(), any(), any())).thenReturn(false,
             true);
         this.request.put("limit", "2");
-        when(this.queryService.hql(anyString())).thenReturn(this.query);
-        when(this.query.setLimit(anyInt())).thenReturn(this.query);
-        when(this.query.setOffset(anyInt())).thenReturn(this.query);
-        when(this.query.bindValues(any(Map.class))).thenReturn(this.query);
-        when(this.query.bindValues(any(List.class))).thenReturn(this.query);
-        when(this.query.count()).thenReturn(2L);
+        initDefaultQueryMocks(2);
         when(this.query.execute()).thenReturn(Arrays.asList("XWiki.NotViewable", "XWiki.Viewable"));
 
         Map<String, Object> result = getJsonResultMap();
@@ -139,6 +138,89 @@ void nonViewableResultsAreObfuscated() throws Exception
         assertEquals("xwiki:XWiki.Viewable", viewable.get("doc_fullName"));
     }
 
+    /**
+     * Request the {@code doc.date} field, filtered by a date range using ISO 8601 based time intervals.
+     */
+    @Test
+    void dateFilterBetweenISO8601() throws Exception
+    {
+        initDefaultQueryMocks(0);
+
+        this.request.put("offset", "1");
+        this.request.put("limit", "15");
+        this.request.put("collist", "doc.date");
+        this.request.put("doc.date_match", "between");
+        this.request.put("doc.date/join_mode", "OR");
+        this.request.put("childrenOf", "Sandbox");
+        this.request.put("doc.date", "2021-09-22T00:00:00+02:00/2021-09-22T23:59:59+02:00");
+        this.templateManager.render(GETDOCUMENTS);
+        verify(this.queryService).hql(
+            "WHERE 1=1 AND doc.fullName LIKE ?1 AND doc.fullName <> ?2 and doc.date between ?3 and ?4 ");
+        List<Object> queryParams = (List<Object>) this.velocityManager.getVelocityContext().get("queryParams");
+        assertNull(queryParams.get(0));
+        assertEquals("Sandbox.WebHome", queryParams.get(1));
+        assertEquals("Wed Sep 22 00:00:00 CEST 2021", queryParams.get(2).toString());
+        assertEquals("Wed Sep 22 23:59:59 CEST 2021", queryParams.get(3).toString());
+    }
+
+    /**
+     * Request the {@code doc.date} field, filtered by a date range using timestamp based time intervals.
+     */
+    @Test
+    void dateFilterBetweenTimestamp() throws Exception
+    {
+        initDefaultQueryMocks(0);
+
+        this.request.put("outputSyntax", "plain");
+        this.request.put("transprefix", "platform.index.");
+        this.request.put("classname", "");
+        this.request.put("collist", "doc.title,doc.location,doc.date,doc.author,_likes");
+        this.request.put("queryFilters", "currentlanguage,hidden");
+        this.request.put("offset", "1");
+        this.request.put("limit", "15");
+        this.request.put("reqNo", "3");
+        this.request.put("doc.date", "1632348000000-1632434399999");
+        this.request.put("sort", "doc.date");
+        this.request.put("dir", "asc");
+        this.templateManager.render(GETDOCUMENTS);
+        verify(this.queryService).hql(
+            "WHERE 1=1 and doc.date between ?1 and ?2 order by doc.date asc");
+        List<Object> queryParams = (List<Object>) this.velocityManager.getVelocityContext().get("queryParams");
+        assertEquals("Thu Sep 23 00:00:00 CEST 2021", queryParams.get(0).toString());
+        assertEquals("Thu Sep 23 23:59:59 CEST 2021", queryParams.get(1).toString());
+    }
+
+    @Test
+    void preventDOSAttackOnQueryItemsReturned() throws Exception
+    {
+        // Simulating the fact that when getdocuments.vm executes, the xwikivars.vm template has already been loaded by
+        // the page rendering.
+        this.templateManager.render("xwikivars.vm");
+
+        this.request.put("limit", "101");
+        when(this.queryService.hql(anyString())).thenReturn(this.query);
+        when(this.query.setLimit(anyInt())).thenReturn(this.query);
+        when(this.query.setOffset(anyInt())).thenReturn(this.query);
+        when(this.query.bindValues(any(Map.class))).thenReturn(this.query);
+        when(this.query.bindValues(any(List.class))).thenReturn(this.query);
+
+        // Simulate the query limit
+        SecurityConfiguration securityConfiguration =
+            this.oldcore.getMocker().registerMockComponent(SecurityConfiguration.class);
+        when(securityConfiguration.getQueryItemsLimit()).thenReturn(100);
+
+        this.templateManager.render(GETDOCUMENTS);
+
+        ArgumentCaptor<Integer> argument = ArgumentCaptor.forClass(Integer.class);
+        verify(this.query).setLimit(argument.capture());
+
+        // Verify that even though the guest user is asking for 101 items, we only return 100.
+        assertEquals(100, argument.getValue());
+    }
+
+    /**
+     * @return the captured JSON map before serialization, to make it easier for each test to assert the map content.
+     */
     private Map<String, Object> getJsonResultMap() throws Exception
     {
         JSONTool jsonTool = mock(JSONTool.class);
@@ -151,4 +233,14 @@ private Map<String, Object> getJsonResultMap() throws Exception
 
         return (Map<String, Object>) argument.getValue();
     }
+
+    private void initDefaultQueryMocks(long count) throws QueryException
+    {
+        when(this.queryService.hql(anyString())).thenReturn(this.query);
+        when(this.query.setLimit(anyInt())).thenReturn(this.query);
+        when(this.query.setOffset(anyInt())).thenReturn(this.query);
+        when(this.query.bindValues(any(Map.class))).thenReturn(this.query);
+        when(this.query.bindValues(any(List.class))).thenReturn(this.query);
+        when(this.query.count()).thenReturn(count);
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/test/java/org/xwiki/web/MacrosTest.java b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/test/java/org/xwiki/web/MacrosTest.java
index 0c39704632722061f373e95798b105f75d7bec0f..3bd74883b5c3f7a4913afe09c04439e88654debc 100644
--- a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/test/java/org/xwiki/web/MacrosTest.java
+++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/test/java/org/xwiki/web/MacrosTest.java
@@ -25,15 +25,19 @@
 import java.util.List;
 import java.util.Map;
 
+import org.apache.velocity.tools.generic.NumberTool;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.xwiki.test.annotation.ComponentList;
 import org.xwiki.test.page.PageTest;
 import org.xwiki.velocity.VelocityManager;
+import org.xwiki.velocity.internal.XWikiDateTool;
 import org.xwiki.velocity.tools.EscapeTool;
 
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonMap;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -42,6 +46,7 @@
  *
  * @version $Id$
  */
+@ComponentList(XWikiDateTool.class)
 class MacrosTest extends PageTest
 {
     private VelocityManager velocityManager;
@@ -49,8 +54,10 @@ class MacrosTest extends PageTest
     @BeforeEach
     void setup() throws Exception
     {
-        this.velocityManager = oldcore.getMocker().getInstance(VelocityManager.class);
-        this.velocityManager.getVelocityContext().put("escapetool", new EscapeTool());
+        this.velocityManager = this.oldcore.getMocker().getInstance(VelocityManager.class);
+        registerVelocityTool("escapetool", new EscapeTool());
+        registerVelocityTool("datetool", this.componentManager.getInstance(XWikiDateTool.class));
+        registerVelocityTool("numbertool", new NumberTool());
     }
 
     @Test
@@ -140,4 +147,76 @@ void livetableFilterObfuscatedTotalLowerThanReturnedRows() throws Exception
         assertEquals(1, ((List<?>) map.get("rows")).size());
         assertTrue(((List<Map<String, Boolean>>) map.get("rows")).get(0).get("doc_viewable"));
     }
+
+    @Test
+    void parseDateRangeAfter() throws Exception
+    {
+        String dateValue = "2021-09-22T00:00:00+02:00";
+
+        this.velocityManager.getVelocityContext().put("dateRange", new HashMap<>());
+        this.velocityManager.getVelocityContext().put("dateValue", dateValue);
+
+        String script = "#parseDateRange('after' $dateValue $dateRange)";
+        StringWriter out = new StringWriter();
+        this.velocityManager.evaluate(out, "parseDateRangeAfter", new StringReader(script));
+
+        Map<Object, Object> dateRange =
+            (Map<Object, Object>) this.velocityManager.getVelocityContext().get("dateRange");
+        assertEquals("Wed Sep 22 00:00:00 CEST 2021", dateRange.get("start").toString());
+        assertNull(dateRange.get("end"));
+    }
+
+    @Test
+    void parseDateRangeBefore() throws Exception
+    {
+        String dateValue = "2021-09-22T23:59:59+02:00";
+
+        this.velocityManager.getVelocityContext().put("dateRange", new HashMap<>());
+        this.velocityManager.getVelocityContext().put("dateValue", dateValue);
+
+        String script = "#parseDateRange('before' $dateValue $dateRange)";
+        StringWriter out = new StringWriter();
+        this.velocityManager.evaluate(out, "parseDateRangeAfter", new StringReader(script));
+
+        Map<Object, Object> dateRange =
+            (Map<Object, Object>) this.velocityManager.getVelocityContext().get("dateRange");
+        assertNull(dateRange.get("start"));
+        assertEquals("Wed Sep 22 23:59:59 CEST 2021", dateRange.get("end").toString());
+    }
+
+    @Test
+    void parseDateRangeBetween() throws Exception
+    {
+        String dateValue = "2021-09-22T00:00:00+02:00/2021-09-22T23:59:59+02:00";
+
+        this.velocityManager.getVelocityContext().put("dateRange", new HashMap<>());
+        this.velocityManager.getVelocityContext().put("dateValue", dateValue);
+
+        String script = "#parseDateRange('between' $dateValue $dateRange)";
+        StringWriter out = new StringWriter();
+        this.velocityManager.evaluate(out, "parseDateRangeAfter", new StringReader(script));
+
+        Map<Object, Object> dateRange =
+            (Map<Object, Object>) this.velocityManager.getVelocityContext().get("dateRange");
+        assertEquals("Wed Sep 22 00:00:00 CEST 2021", dateRange.get("start").toString());
+        assertEquals("Wed Sep 22 23:59:59 CEST 2021", dateRange.get("end").toString());
+    }
+
+    @Test
+    void parseDateRangeTimestampRange() throws Exception
+    {
+        String dateValue = "1607295600000-1632347999999";
+
+        this.velocityManager.getVelocityContext().put("dateRange", new HashMap<>());
+        this.velocityManager.getVelocityContext().put("dateValue", dateValue);
+
+        String script = "#parseDateRange('after' $dateValue $dateRange)";
+        StringWriter out = new StringWriter();
+        this.velocityManager.evaluate(out, "parseDateRangeAfter", new StringReader(script));
+
+        Map<Object, Object> dateRange =
+            (Map<Object, Object>) this.velocityManager.getVelocityContext().get("dateRange");
+        assertEquals("Mon Dec 07 00:00:00 CET 2020", dateRange.get("start").toString());
+        assertEquals("Wed Sep 22 23:59:59 CEST 2021", dateRange.get("end").toString());
+    }
 }