4040namespace OCA \DAV \CalDAV ;
4141
4242use DateTime ;
43+ use DateTimeImmutable ;
4344use DateTimeInterface ;
4445use OCA \DAV \AppInfo \Application ;
4546use OCA \DAV \CalDAV \Sharing \Backend ;
@@ -1920,15 +1921,34 @@ public function search(
19201921 $ this ->db ->escapeLikeParameter ($ pattern ) . '% ' )));
19211922 }
19221923
1923- if (isset ($ options ['timerange ' ])) {
1924- if (isset ($ options ['timerange ' ]['start ' ]) && $ options ['timerange ' ]['start ' ] instanceof DateTimeInterface) {
1925- $ outerQuery ->andWhere ($ outerQuery ->expr ()->gt ('lastoccurence ' ,
1926- $ outerQuery ->createNamedParameter ($ options ['timerange ' ]['start ' ]->getTimeStamp ())));
1927- }
1928- if (isset ($ options ['timerange ' ]['end ' ]) && $ options ['timerange ' ]['end ' ] instanceof DateTimeInterface) {
1929- $ outerQuery ->andWhere ($ outerQuery ->expr ()->lt ('firstoccurence ' ,
1930- $ outerQuery ->createNamedParameter ($ options ['timerange ' ]['end ' ]->getTimeStamp ())));
1931- }
1924+ $ start = null ;
1925+ $ end = null ;
1926+
1927+ $ hasLimit = is_int ($ limit );
1928+ $ hasTimeRange = false ;
1929+
1930+ if (isset ($ options ['timerange ' ]['start ' ]) && $ options ['timerange ' ]['start ' ] instanceof DateTimeInterface) {
1931+ /** @var DateTimeInterface $start */
1932+ $ start = $ options ['timerange ' ]['start ' ];
1933+ $ outerQuery ->andWhere (
1934+ $ outerQuery ->expr ()->gt (
1935+ 'lastoccurence ' ,
1936+ $ outerQuery ->createNamedParameter ($ start ->getTimestamp ())
1937+ )
1938+ );
1939+ $ hasTimeRange = true ;
1940+ }
1941+
1942+ if (isset ($ options ['timerange ' ]['end ' ]) && $ options ['timerange ' ]['end ' ] instanceof DateTimeInterface) {
1943+ /** @var DateTimeInterface $end */
1944+ $ end = $ options ['timerange ' ]['end ' ];
1945+ $ outerQuery ->andWhere (
1946+ $ outerQuery ->expr ()->lt (
1947+ 'firstoccurence ' ,
1948+ $ outerQuery ->createNamedParameter ($ end ->getTimestamp ())
1949+ )
1950+ );
1951+ $ hasTimeRange = true ;
19321952 }
19331953
19341954 if (isset ($ options ['uid ' ])) {
@@ -1946,54 +1966,46 @@ public function search(
19461966
19471967 $ outerQuery ->andWhere ($ outerQuery ->expr ()->in ('c.id ' , $ outerQuery ->createFunction ($ innerQuery ->getSQL ())));
19481968
1949- if ($ offset ) {
1950- $ outerQuery ->setFirstResult ($ offset );
1951- }
1952- if ($ limit ) {
1953- $ outerQuery ->setMaxResults ($ limit );
1954- }
1969+ // Without explicit order by its undefined in which order the SQL server returns the events.
1970+ // For the pagination with hasLimit and hasTimeRange, a stable ordering is helpful.
1971+ $ outerQuery ->addOrderBy ('id ' );
19551972
1956- $ result = $ outerQuery ->executeQuery ();
1957- $ calendarObjects = [];
1958- while (($ row = $ result ->fetch ()) !== false ) {
1959- $ start = $ options ['timerange ' ]['start ' ] ?? null ;
1960- $ end = $ options ['timerange ' ]['end ' ] ?? null ;
1973+ $ offset = (int )$ offset ;
1974+ $ outerQuery ->setFirstResult ($ offset );
19611975
1962- if ($ start === null || !($ start instanceof DateTimeInterface) || $ end === null || !($ end instanceof DateTimeInterface)) {
1963- // No filter required
1964- $ calendarObjects [] = $ row ;
1965- continue ;
1966- }
1976+ $ calendarObjects = [];
19671977
1968- $ isValid = $ this ->validateFilterForObject ($ row , [
1969- 'name ' => 'VCALENDAR ' ,
1970- 'comp-filters ' => [
1971- [
1972- 'name ' => 'VEVENT ' ,
1973- 'comp-filters ' => [],
1974- 'prop-filters ' => [],
1975- 'is-not-defined ' => false ,
1976- 'time-range ' => [
1977- 'start ' => $ start ,
1978- 'end ' => $ end ,
1979- ],
1980- ],
1981- ],
1982- 'prop-filters ' => [],
1983- 'is-not-defined ' => false ,
1984- 'time-range ' => null ,
1985- ]);
1986- if (is_resource ($ row ['calendardata ' ])) {
1987- // Put the stream back to the beginning so it can be read another time
1988- rewind ($ row ['calendardata ' ]);
1989- }
1990- if ($ isValid ) {
1991- $ calendarObjects [] = $ row ;
1992- }
1978+ if ($ hasLimit && $ hasTimeRange ) {
1979+ /**
1980+ * Event recurrences are evaluated at runtime because the database only knows the first and last occurrence.
1981+ *
1982+ * Given, a user created 8 events with a yearly reoccurrence and two for events tomorrow.
1983+ * The upcoming event widget asks the CalDAV backend for 7 events within the next 14 days.
1984+ *
1985+ * If limit 7 is applied to the SQL query, we find the 7 events with a yearly reoccurrence
1986+ * and discard the events after evaluating the reoccurrence rules because they are not due within
1987+ * the next 14 days and end up with an empty result even if there are two events to show.
1988+ *
1989+ * The workaround for search requests with a limit and time range is asking for more row than requested
1990+ * and retrying if we have not reached the limit.
1991+ *
1992+ * 25 rows and 3 retries is entirely arbitrary.
1993+ */
1994+ $ maxResults = (int )max ($ limit , 25 );
1995+ $ outerQuery ->setMaxResults ($ maxResults );
1996+
1997+ for ($ attempt = $ objectsCount = 0 ; $ attempt < 3 && $ objectsCount < $ limit ; $ attempt ++) {
1998+ $ objectsCount = array_push ($ calendarObjects , ...$ this ->searchCalendarObjects ($ outerQuery , $ start , $ end ));
1999+ $ outerQuery ->setFirstResult ($ offset += $ maxResults );
2000+ }
2001+
2002+ $ calendarObjects = array_slice ($ calendarObjects , 0 , $ limit , false );
2003+ } else {
2004+ $ outerQuery ->setMaxResults ($ limit );
2005+ $ calendarObjects = $ this ->searchCalendarObjects ($ outerQuery , $ start , $ end );
19932006 }
1994- $ result ->closeCursor ();
19952007
1996- return array_map (function ($ o ) use ($ options ) {
2008+ $ calendarObjects = array_map (function ($ o ) use ($ options ) {
19972009 $ calendarData = Reader::read ($ o ['calendardata ' ]);
19982010
19992011 // Expand recurrences if an explicit time range is requested
@@ -2029,6 +2041,64 @@ public function search(
20292041 }, $ timezones ),
20302042 ];
20312043 }, $ calendarObjects );
2044+
2045+ usort ($ calendarObjects , function (array $ a , array $ b ) {
2046+ /** @var DateTimeImmutable $startA */
2047+ $ startA = $ a ['objects ' ][0 ]['DTSTART ' ][0 ] ?? new DateTimeImmutable (self ::MAX_DATE );
2048+ /** @var DateTimeImmutable $startB */
2049+ $ startB = $ b ['objects ' ][0 ]['DTSTART ' ][0 ] ?? new DateTimeImmutable (self ::MAX_DATE );
2050+
2051+ return $ startA ->getTimestamp () <=> $ startB ->getTimestamp ();
2052+ });
2053+
2054+ return $ calendarObjects ;
2055+ }
2056+
2057+ private function searchCalendarObjects (IQueryBuilder $ query , DateTimeInterface |null $ start , DateTimeInterface |null $ end ): array {
2058+ $ calendarObjects = [];
2059+ $ filterByTimeRange = ($ start instanceof DateTimeInterface) || ($ end instanceof DateTimeInterface);
2060+
2061+ $ result = $ query ->executeQuery ();
2062+
2063+ while (($ row = $ result ->fetch ()) !== false ) {
2064+ if ($ filterByTimeRange === false ) {
2065+ // No filter required
2066+ $ calendarObjects [] = $ row ;
2067+ continue ;
2068+ }
2069+
2070+ $ isValid = $ this ->validateFilterForObject ($ row , [
2071+ 'name ' => 'VCALENDAR ' ,
2072+ 'comp-filters ' => [
2073+ [
2074+ 'name ' => 'VEVENT ' ,
2075+ 'comp-filters ' => [],
2076+ 'prop-filters ' => [],
2077+ 'is-not-defined ' => false ,
2078+ 'time-range ' => [
2079+ 'start ' => $ start ,
2080+ 'end ' => $ end ,
2081+ ],
2082+ ],
2083+ ],
2084+ 'prop-filters ' => [],
2085+ 'is-not-defined ' => false ,
2086+ 'time-range ' => null ,
2087+ ]);
2088+
2089+ if (is_resource ($ row ['calendardata ' ])) {
2090+ // Put the stream back to the beginning so it can be read another time
2091+ rewind ($ row ['calendardata ' ]);
2092+ }
2093+
2094+ if ($ isValid ) {
2095+ $ calendarObjects [] = $ row ;
2096+ }
2097+ }
2098+
2099+ $ result ->closeCursor ();
2100+
2101+ return $ calendarObjects ;
20322102 }
20332103
20342104 /**
0 commit comments