* CalDAV/CardDAV/REST API: fix not working limited result

(since using generator instead of iterator)
This commit is contained in:
ralf 2024-05-13 19:18:44 +02:00
parent 9eee63bbbd
commit 8864d6ed49
6 changed files with 122 additions and 91 deletions

View File

@ -170,31 +170,22 @@ class addressbook_groupdav extends Api\CalDAV\Handler
} }
} }
// rfc 6578 sync-collection report: filter for sync-token is already set in _report_filters // rfc 6578 sync-collection report: filter for sync-token is already set in _report_filters
if ($options['root']['name'] == 'sync-collection') if ($options['root']['name'] === 'sync-collection')
{ {
// callback to query sync-token, after propfind_callbacks / iterator is run and // callback to query sync-token, after propfind_callbacks / iterator is run and
// stored max. modification-time in $this->sync_collection_token // stored max. modification-time in $this->sync_collection_token
$files['sync-token'] = array($this, 'get_sync_collection_token'); $files['sync-token'] = array($this, 'get_sync_collection_token');
$files['sync-token-params'] = array($path, $user); $files['sync-token-params'] = array($path, $user);
$this->sync_collection_token = null; $this->sync_collection_token = $this->more_results = null;
$filter['order'] = 'contact_modified ASC'; // return oldest modifications first $filter['order'] = 'contact_modified ASC'; // return oldest modifications first
$filter['sync-collection'] = true; $filter['sync-collection'] = true;
} }
if (isset($nresults)) if (isset($nresults) && $options['root']['name'] === 'sync-collection')
{ {
$files['files'] = $this->propfind_generator($path, $filter, $files['files'], (int)$nresults); $files['files'] = $this->propfind_generator($path, $filter, $files['files'], (int)$nresults);
// hack to support limit with sync-collection report: contacts are returned in modified ASC order (oldest first)
// if limit is smaller than full result, return modified-1 as sync-token, so client requests next chunk incl. modified
// (which might contain further entries with identical modification time)
if ($options['root']['name'] == 'sync-collection' && $this->bo->total > $nresults)
{
--$this->sync_collection_token;
$files['sync-token-params'][] = true; // tell get_sync_collection_token that we have more entries
}
} }
else else
{ {
@ -232,6 +223,8 @@ class addressbook_groupdav extends Api\CalDAV\Handler
{ {
if (++$yielded && isset($nresults) && $yielded > $nresults) if (++$yielded && isset($nresults) && $yielded > $nresults)
{ {
$this->sync_collection_token = $resource['modified'];
$this->more_results = true;
return; return;
} }
yield $resource; yield $resource;
@ -307,13 +300,15 @@ class addressbook_groupdav extends Api\CalDAV\Handler
{ {
unset($this->requested_multiget_ids[$k]); unset($this->requested_multiget_ids[$k]);
} }
if (++$yielded && isset($nresults) && $yielded > $nresults)
{
$this->sync_collection_token = $contact['modified'];
$this->more_results = true;
return;
}
// sync-collection report: deleted entry need to be reported without properties // sync-collection report: deleted entry need to be reported without properties
if ($contact['tid'] == Api\Contacts::DELETED_TYPE) if ($contact['tid'] == Api\Contacts::DELETED_TYPE)
{ {
if (++$yielded && isset($nresults) && $yielded > $nresults)
{
return;
}
yield ['path' => $path.urldecode($this->get_path($contact))]; yield ['path' => $path.urldecode($this->get_path($contact))];
continue; continue;
} }
@ -329,10 +324,6 @@ class addressbook_groupdav extends Api\CalDAV\Handler
$props['getcontentlength'] = bytes(is_array($content) ? json_encode($content) : $content); $props['getcontentlength'] = bytes(is_array($content) ? json_encode($content) : $content);
$props['address-data'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content); $props['address-data'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content);
} }
if (++$yielded && isset($nresults) && $yielded > $nresults)
{
return;
}
yield $this->add_resource($path, $contact, $props); yield $this->add_resource($path, $contact, $props);
} }
// sync-collection report --> return modified of last contact as sync-token // sync-collection report --> return modified of last contact as sync-token
@ -354,6 +345,8 @@ class addressbook_groupdav extends Api\CalDAV\Handler
{ {
if (++$yielded && isset($nresults) && $yielded > $nresults) if (++$yielded && isset($nresults) && $yielded > $nresults)
{ {
$this->sync_collection_token = Api\DateTime::user2server($resource['modified'])-1;
$this->more_results = true;
return; return;
} }
yield $resource; yield $resource;
@ -408,6 +401,8 @@ class addressbook_groupdav extends Api\CalDAV\Handler
} }
if (++$yielded && isset($nresults) && $yielded > $nresults) if (++$yielded && isset($nresults) && $yielded > $nresults)
{ {
$this->sync_collection_token = $GLOBALS['egw']->db->from_timestamp($list['list_modified'])-1;
$this->more_results = true;
return; return;
} }
yield $this->add_resource($path, $list, $props); yield $this->add_resource($path, $list, $props);
@ -433,6 +428,8 @@ class addressbook_groupdav extends Api\CalDAV\Handler
{ {
if (++$yielded && isset($nresults) && $yielded > $nresults) if (++$yielded && isset($nresults) && $yielded > $nresults)
{ {
--$this->sync_collection_token;
$this->more_results = true;
return; return;
} }
yield ['path' => $path.$id.self::$path_extension]; yield ['path' => $path.$id.self::$path_extension];

View File

@ -724,6 +724,10 @@ abstract class Handler
* sync-token to be filled by propfind_callback and returned by get_sync_token method * sync-token to be filled by propfind_callback and returned by get_sync_token method
*/ */
protected $sync_collection_token; protected $sync_collection_token;
/**
* @var bool true if result was limited by nresults parameter
*/
protected $more_results;
/** /**
* Query sync-token from a just run sync-collection report * Query sync-token from a just run sync-collection report
@ -734,10 +738,10 @@ abstract class Handler
* @param int $user parameter necessary to call getctag, if no $token specified * @param int $user parameter necessary to call getctag, if no $token specified
* @return string * @return string
*/ */
public function get_sync_collection_token($path, $user=null, $more_results=null) public function get_sync_collection_token($path, $user=null)
{ {
//error_log(__METHOD__."('$path', $user, more_results=$more_results) this->sync_collection_token=".$this->sync_collection_token); //error_log(__METHOD__."('$path', $user, more_results=$more_results) this->sync_collection_token=".$this->sync_collection_token);
if ($more_results) if ($this->more_results)
{ {
if (Api\CalDAV::isJSON()) if (Api\CalDAV::isJSON())
{ {

View File

@ -238,14 +238,14 @@ class calendar_groupdav extends Api\CalDAV\Handler
} }
// rfc 6578 sync-collection report: filter for sync-token is already set in _report_filters // rfc 6578 sync-collection report: filter for sync-token is already set in _report_filters
if ($options['root']['name'] == 'sync-collection') if ($options['root']['name'] === 'sync-collection')
{ {
// callback to query sync-token, after propfind_callbacks / iterator is run and // callback to query sync-token, after propfind_callbacks / iterator is run and
// stored max. modification-time in $this->sync_collection_token // stored max. modification-time in $this->sync_collection_token
$files['sync-token'] = array($this, 'get_sync_collection_token'); $files['sync-token'] = array($this, 'get_sync_collection_token');
$files['sync-token-params'] = array($path, $user); $files['sync-token-params'] = array($path, $user);
$this->sync_collection_token = null; $this->sync_collection_token = $this->more_results = null;
$filter['order'] = 'cal_modified ASC'; // return oldest modifications first $filter['order'] = 'cal_modified ASC'; // return oldest modifications first
$filter['sync-collection'] = true; $filter['sync-collection'] = true;
@ -267,19 +267,10 @@ class calendar_groupdav extends Api\CalDAV\Handler
} }
} }
if (isset($nresults)) if (isset($nresults) && $options['root']['name'] === 'sync-collection')
{ {
unset($filter['no_total']); // we need the total! unset($filter['no_total']); // we need the total!
$files['files'] = $this->propfind_generator($path, $filter, $files['files'], (int)$nresults); $files['files'] = $this->propfind_generator($path, $filter, $files['files'], (int)$nresults);
// hack to support limit with sync-collection report: events are returned in modified ASC order (oldest first)
// if limit is smaller than full result, return modified-1 as sync-token, so client requests next chunk incl. modified
// (which might contain further entries with identical modification time)
if ($options['root']['name'] == 'sync-collection' && $this->bo->total > $nresults)
{
--$this->sync_collection_token;
$files['sync-token-params'][] = true; // tel get_sync_collection_token that we have more entries
}
} }
else else
{ {
@ -366,6 +357,8 @@ class calendar_groupdav extends Api\CalDAV\Handler
{ {
if (++$yielded && isset($nresults) && $yielded > $nresults) if (++$yielded && isset($nresults) && $yielded > $nresults)
{ {
$this->sync_collection_token = Api\DateTime::user2server($resource['modified'], 'ts')-1;
$this->more_results = true;
return; return;
} }
yield $resource; yield $resource;
@ -385,15 +378,17 @@ class calendar_groupdav extends Api\CalDAV\Handler
{ {
$no_active_participants = !$this->hasActiveParticipants($event, $filter['users'], $exceptions); $no_active_participants = !$this->hasActiveParticipants($event, $filter['users'], $exceptions);
if (++$yielded && isset($nresults) && $yielded > $nresults)
{
$this->sync_collection_token = Api\DateTime::user2server($event['modified'], 'ts')-1;
$this->more_results = true;
return;
}
// sync-collection report: deleted entries need to be reported without properties, same for rejected or deleted invitations // sync-collection report: deleted entries need to be reported without properties, same for rejected or deleted invitations
if ($sync_collection && ($event['deleted'] && !$event['cal_reference'] || if ($sync_collection && ($event['deleted'] && !$event['cal_reference'] ||
//in_array($event['participants'][$filter['users']][0] ?? '', array('R','X')) || //in_array($event['participants'][$filter['users']][0] ?? '', array('R','X')) ||
$no_active_participants)) $no_active_participants))
{ {
if (++$yielded && isset($nresults) && $yielded > $nresults)
{
return;
}
// remove event from requested multiget ids, to be able to report not found urls // remove event from requested multiget ids, to be able to report not found urls
if (!empty($this->requested_multiget_ids) && ($k = array_search($event[self::$path_attr], $this->requested_multiget_ids)) !== false) if (!empty($this->requested_multiget_ids) && ($k = array_search($event[self::$path_attr], $this->requested_multiget_ids)) !== false)
{ {
@ -420,7 +415,8 @@ class calendar_groupdav extends Api\CalDAV\Handler
'getcontenttype' => $is_jscalendar ? Api\CalDAV\JsCalendar::MIME_TYPE_JSEVENT : 'getcontenttype' => $is_jscalendar ? Api\CalDAV\JsCalendar::MIME_TYPE_JSEVENT :
($this->agent != 'kde' ? 'text/calendar; charset=utf-8; component=VEVENT' : 'text/calendar'), ($this->agent != 'kde' ? 'text/calendar; charset=utf-8; component=VEVENT' : 'text/calendar'),
'getetag' => '"'.$etag.'"', 'getetag' => '"'.$etag.'"',
'getlastmodified' => $event['modified'], 'displayname' => $event['title'],
'getlastmodified' => Api\DateTime::user2server($event['modified'], 'ts'),
// user and timestamp of creation or last modification of event, used in calendarserver only for shared calendars // user and timestamp of creation or last modification of event, used in calendarserver only for shared calendars
'created-by' => Api\CalDAV::mkprop(Api\CalDAV::CALENDARSERVER, 'created-by', 'created-by' => Api\CalDAV::mkprop(Api\CalDAV::CALENDARSERVER, 'created-by',
$this->_created_updated_by_prop($event['creator'], $event['created'])), $this->_created_updated_by_prop($event['creator'], $event['created'])),
@ -452,10 +448,6 @@ class calendar_groupdav extends Api\CalDAV\Handler
)), )),
)); ));
}*/ }*/
if (++$yielded && isset($nresults) && $yielded > $nresults)
{
return;
}
yield $this->add_resource($path, $event, $props); yield $this->add_resource($path, $event, $props);
} }
} }

View File

@ -208,7 +208,7 @@ class infolog_groupdav extends Api\CalDAV\Handler
$files['sync-token'] = array($this, 'get_sync_collection_token'); $files['sync-token'] = array($this, 'get_sync_collection_token');
$files['sync-token-params'] = array($path, $user); $files['sync-token-params'] = array($path, $user);
$this->sync_collection_token = null; $this->sync_collection_token = $this->more_results = null;
$filter['order'] = 'info_datemodified ASC'; // return oldest modifications first $filter['order'] = 'info_datemodified ASC'; // return oldest modifications first
$filter['sync-collection'] = true; $filter['sync-collection'] = true;
@ -217,15 +217,6 @@ class infolog_groupdav extends Api\CalDAV\Handler
if (isset($nresults)) if (isset($nresults))
{ {
$files['files'] = $this->propfind_generator($path, $filter, [], $nresults); $files['files'] = $this->propfind_generator($path, $filter, [], $nresults);
// hack to support limit with sync-collection report: contacts are returned in modified ASC order (oldest first)
// if limit is smaller than full result, return modified-1 as sync-token, so client requests next chunk incl. modified
// (which might contain further entries with identical modification time)
if ($options['root']['name'] == 'sync-collection' && $this->bo->total > $nresults)
{
--$this->sync_collection_token;
$files['sync-token-params'][] = true; // tel get_sync_collection_token that we have more entries
}
} }
else else
{ {
@ -267,6 +258,8 @@ class infolog_groupdav extends Api\CalDAV\Handler
{ {
if (++$yielded && isset($nresults) && $yielded > $nresults) if (++$yielded && isset($nresults) && $yielded > $nresults)
{ {
$this->sync_collection_token = Api\DateTime::user2server($resource['modified'], 'ts')-1;
$this->more_results = true;
return; return;
} }
yield $resource; yield $resource;
@ -321,16 +314,18 @@ class infolog_groupdav extends Api\CalDAV\Handler
{ {
unset($this->requested_multiget_ids[$k]); unset($this->requested_multiget_ids[$k]);
} }
if (++$yielded && isset($nresults) && $yielded > $nresults)
{
$this->sync_collection_token = Api\DateTime::user2server($task['info_modified'], 'ts')-1;
$this->more_results = true;
return;
}
// sync-collection report: deleted entry need to be reported without properties // sync-collection report: deleted entry need to be reported without properties
if ($task['info_status'] == 'deleted' || if ($task['info_status'] == 'deleted' ||
// or event is reported as removed from collection, because collection owner is no longer an attendee // or event is reported as removed from collection, because collection owner is no longer an attendee
$check_responsible && $task['info_owner'] != $check_responsible && $check_responsible && $task['info_owner'] != $check_responsible &&
!infolog_so::is_responsible_user($task, $check_responsible)) !infolog_so::is_responsible_user($task, $check_responsible))
{ {
if (++$yielded && isset($nresults) && $yielded > $nresults)
{
return;
}
yield ['path' => $path.urldecode($this->get_path($task))]; yield ['path' => $path.urldecode($this->get_path($task))];
continue; continue;
} }
@ -354,10 +349,6 @@ class infolog_groupdav extends Api\CalDAV\Handler
$props['calendar-data'] = Api\CalDAV::mkprop(Api\CalDAV::CALDAV,'calendar-data',$content); $props['calendar-data'] = Api\CalDAV::mkprop(Api\CalDAV::CALDAV,'calendar-data',$content);
} }
} }
if (++$yielded && isset($nresults) && $yielded > $nresults)
{
return;
}
yield $this->add_resource($path, $task, $props); yield $this->add_resource($path, $task, $props);
} }
// Please note: $query['start'] get incremented automatically by bo->search() with number of returned rows! // Please note: $query['start'] get incremented automatically by bo->search() with number of returned rows!
@ -367,6 +358,12 @@ class infolog_groupdav extends Api\CalDAV\Handler
break; break;
} }
} }
// sync-collection report --> return modified of last contact as sync-token
$sync_collection_report = $filter['sync-collection'];
if ($sync_collection_report)
{
$this->sync_collection_token = $task['date_modified'];
}
// report not found multiget urls // report not found multiget urls
if (!empty($this->requested_multiget_ids)) if (!empty($this->requested_multiget_ids))
{ {
@ -374,17 +371,13 @@ class infolog_groupdav extends Api\CalDAV\Handler
{ {
if (++$yielded && isset($nresults) && $yielded > $nresults) if (++$yielded && isset($nresults) && $yielded > $nresults)
{ {
--$this->sync_collection_token;
$this->more_results = true;
return; return;
} }
yield ['path' => $path.$id.self::$path_extension]; yield ['path' => $path.$id.self::$path_extension];
} }
} }
// sync-collection report --> return modified of last contact as sync-token
$sync_collection_report = $filter['sync-collection'];
if ($sync_collection_report)
{
$this->sync_collection_token = $task['date_modified'];
}
if ($this->debug) if ($this->debug)
{ {
error_log(__METHOD__."($path) took ".(microtime(true) - $starttime)." to return $yielded resources, filter[sync-collection]=$sync_collection_report, sync-token=$this->sync_collection_token"); error_log(__METHOD__."($path) took ".(microtime(true) - $starttime)." to return $yielded resources, filter[sync-collection]=$sync_collection_report, sync-token=$this->sync_collection_token");

View File

@ -486,7 +486,14 @@ class timesheet_bo extends Api\Storage
} }
if(isset($filter['ts_status']) && $filter['ts_status'] && $filter['ts_status'] != self::DELETED_STATUS) if(isset($filter['ts_status']) && $filter['ts_status'] && $filter['ts_status'] != self::DELETED_STATUS)
{ {
$filter['ts_status'] = $this->get_sub_status($filter['ts_status']); if ($filter['ts_status'] !== 'all')
{
$filter['ts_status'] = $this->get_sub_status($filter['ts_status']);
}
else
{
unset($filter['ts_status']);
}
} }
else else
{ {
@ -1112,4 +1119,39 @@ class timesheet_bo extends Api\Storage
} }
return null; return null;
} }
/**
* Get a ctag (collection tag) for timesheet
*
* Currently implemented as maximum modification date (1 second granularity!)
*
* We have to include deleted entries, as otherwise the ctag will not change if an entry gets deleted!
* (Only works if tracking of deleted entries / history is switched on!)
*
* @param int|array $user =null
* @return string
*/
public function getctag($user=null)
{
$filter = array('ts_status' => 'all'); // --> use all entries incl. deleted
// show timesheets of a single user?
if ($user) $filter['ts_owner'] = $user;
$result = $this->search(array(),'ts_modified','ts_modified DESC','','',false,'AND',array(0,1),$filter);
if (!$result || !isset($result[0]['ts_modified']))
{
$ctag = 'empty'; // ctag for empty addressbook
}
else
{
// need to convert modified time back to server-time (was converted to user-time by search)
// as we use it direct in server-queries eg. CardDAV sync-report and to be consistent with CalDAV
$ctag = Api\DateTime::user2server($result[0]['ts_modified']);
}
//error_log(__METHOD__.'('.array2string($owner).') returning '.array2string($ctag));
return $ctag;
}
} }

View File

@ -75,31 +75,22 @@ class ApiHandler extends Api\CalDAV\Handler
if ($this->debug) error_log(__METHOD__."($path,".array2string($options).",,$user,$id) filter=".array2string($filter)); if ($this->debug) error_log(__METHOD__."($path,".array2string($options).",,$user,$id) filter=".array2string($filter));
// rfc 6578 sync-collection report: filter for sync-token is already set in _report_filters // rfc 6578 sync-collection report: filter for sync-token is already set in _report_filters
if ($options['root']['name'] == 'sync-collection') if ($options['root']['name'] === 'sync-collection')
{ {
// callback to query sync-token, after propfind_callbacks / iterator is run and // callback to query sync-token, after propfind_callbacks / iterator is run and
// stored max. modification-time in $this->sync_collection_token // stored max. modification-time in $this->sync_collection_token
$files['sync-token'] = array($this, 'get_sync_collection_token'); $files['sync-token'] = array($this, 'get_sync_collection_token');
$files['sync-token-params'] = array($path, $user); $files['sync-token-params'] = array($path, $user);
$this->sync_collection_token = null; $this->sync_collection_token = $this->more_results = null;
$filter['order'] = 'ts_modified ASC'; // return oldest modifications first $filter['order'] = 'ts_modified ASC'; // return oldest modifications first
$filter['sync-collection'] = true; $filter['sync-collection'] = true;
} }
if (isset($nresults)) if (isset($nresults) && $options['root']['name'] === 'sync-collection')
{ {
$files['files'] = $this->propfind_generator($path, $filter, $files['files'], (int)$nresults); $files['files'] = $this->propfind_generator($path, $filter, $files['files'], (int)$nresults);
// hack to support limit with sync-collection report: timesheets are returned in modified ASC order (oldest first)
// if limit is smaller than full result, return modified-1 as sync-token, so client requests next chunk incl. modified
// (which might contain further entries with identical modification time)
if ($options['root']['name'] == 'sync-collection' && $this->bo->total > $nresults)
{
--$this->sync_collection_token;
$files['sync-token-params'][] = true; // tell get_sync_collection_token that we have more entries
}
} }
else else
{ {
@ -109,6 +100,16 @@ class ApiHandler extends Api\CalDAV\Handler
return true; return true;
} }
/**
* Query ctag for infolog
*
* @return string
*/
public function getctag($path,$user)
{
return $this->bo->getctag($user);
}
/** /**
* Chunk-size for DB queries of profind_generator * Chunk-size for DB queries of profind_generator
*/ */
@ -136,6 +137,8 @@ class ApiHandler extends Api\CalDAV\Handler
{ {
if (++$yielded && isset($nresults) && $yielded > $nresults) if (++$yielded && isset($nresults) && $yielded > $nresults)
{ {
$this->sync_collection_token = Api\DateTime::user2server($resource['modified'], 'ts')-1;
$this->more_results = true;
return; return;
} }
yield $resource; yield $resource;
@ -192,13 +195,15 @@ class ApiHandler extends Api\CalDAV\Handler
{ {
unset($this->requested_multiget_ids[$k]); unset($this->requested_multiget_ids[$k]);
} }
// sync-collection report: deleted entry need to be reported without properties if (++$yielded && isset($nresults) && $yielded > $nresults)
if ($timesheet['ts_status'] == \timesheet_bo::DELETED_STATUS) {
$this->sync_collection_token = Api\DateTime::user2server($timesheet['modified'], 'ts')-1;
$this->more_results = true;
return;
}
// sync-collection report: deleted entry need to be reported without properties
if ($timesheet['status'] == \timesheet_bo::DELETED_STATUS)
{ {
if (++$yielded && isset($nresults) && $yielded > $nresults)
{
return;
}
yield ['path' => $path.urldecode($this->get_path($timesheet))]; yield ['path' => $path.urldecode($this->get_path($timesheet))];
continue; continue;
} }
@ -212,10 +217,6 @@ class ApiHandler extends Api\CalDAV\Handler
$props['getcontentlength'] = bytes(is_array($content) ? json_encode($content) : $content); $props['getcontentlength'] = bytes(is_array($content) ? json_encode($content) : $content);
$props['data'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'data', $content); $props['data'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'data', $content);
} }
if (++$yielded && isset($nresults) && $yielded > $nresults)
{
return;
}
yield $this->add_resource($path, $timesheet, $props); yield $this->add_resource($path, $timesheet, $props);
} }
// sync-collection report --> return modified of last timesheet as sync-token // sync-collection report --> return modified of last timesheet as sync-token
@ -232,6 +233,8 @@ class ApiHandler extends Api\CalDAV\Handler
{ {
if (++$yielded && isset($nresults) && $yielded > $nresults) if (++$yielded && isset($nresults) && $yielded > $nresults)
{ {
--$this->sync_collection_token;
$this->more_results = true;
return; return;
} }
yield ['path' => $path.$id.self::$path_extension]; yield ['path' => $path.$id.self::$path_extension];
@ -456,7 +459,7 @@ class ApiHandler extends Api\CalDAV\Handler
$parts = explode('/', $option['data']); $parts = explode('/', $option['data']);
$sync_token = array_pop($parts); $sync_token = array_pop($parts);
$filters[] = 'ts_modified>'.(int)$sync_token; $filters[] = 'ts_modified>'.(int)$sync_token;
$filters['tid'] = null; // to return deleted entries too $filters['ts_status'] = 'all'; // to return deleted entries too
} }
break; break;
case 'sync-level': case 'sync-level':