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()); + } }