From 038daecb1b65dc81f3d84c6d669e311670e96ffc Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 7 Aug 2018 15:02:31 +0200 Subject: [PATCH] * CalDAV/Calendar: fix not working snozzing of alarms in Thunderbird Caused by triggered alarms were - so far - immediatly deleted, now we keep them around for an other day, so TB get them in the update iCal after PUTing its X-MOZ-SNOOZE-TIME- --- api/src/Asyncservice.php | 84 ++++++++++++++++++++------ calendar/inc/class.calendar_so.inc.php | 14 ++++- 2 files changed, 78 insertions(+), 20 deletions(-) diff --git a/api/src/Asyncservice.php b/api/src/Asyncservice.php index d338145418..446b3ab15c 100644 --- a/api/src/Asyncservice.php +++ b/api/src/Asyncservice.php @@ -9,7 +9,6 @@ * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package api * @access public - * @version $Id$ */ namespace EGroupware\Api; @@ -30,12 +29,22 @@ class Asyncservice */ var $db; var $db_table = 'egw_async'; - var $debug = 0; + /** + * Enable logging to PHP error_log + * + * @var boolean + */ + var $debug = false; /** * Line in crontab set by constructor with absolute path */ var $cronline = '/api/asyncservices.php default'; + /** + * Time to keep expired jobs marked as "keep", until they got finally deleted + */ + const DEFAULT_KEEP_TIME = 86400; // 1day + /** * constructor of the class */ @@ -57,7 +66,7 @@ class Asyncservice /** * calculates the next run of the timer and puts that with the rest of the data in the db for later execution. * - * @param int/array $times unix timestamp or array('min','hour','dow','day','month','year') with execution time. + * @param int|array $times unix timestamp or array('min','hour','dow','day','month','year') with execution time. * Repeated events are possible to shedule by setting the array only partly, eg. * array('day' => 1) for first day in each month 0am or array('min' => '* /5', 'hour' => '9-17') * for every 5mins in the time from 9am to 5pm. @@ -67,16 +76,28 @@ class Asyncservice * '..'. * @param mixed $data =null This data is passed back when the method is called. If it is an array, * EGroupware will add the rest of the job parameters like id, next (sheduled exec time), times, ... + * Integer value for key "keep" (in seconds) can be used to NOT delete the job automatically, after it was triggered. + * Async service with set a async_next value of 0 and key "keep_time" with timestamp on how long to keep the entry around. * @param int $account_id account_id, under which the methode should be called or False for the actual user * @param boolean $debug =false + * @param boolean $allow_past =false allow to set alarms in the past eg. with times===0 to not trigger it in next run * @return boolean False if $id already exists, else True */ - function set_timer($times,$id,$method,$data=null,$account_id=False,$debug=false) + function set_timer($times, $id, $method, $data=null, $account_id=False, $debug=false, $allow_past=false) { if (empty($id) || empty($method) || $this->read($id) || !($next = $this->next_run($times,$debug))) { - return False; + // allow to set "keep" alarms in the past ($next === false) + if ($next === false && !is_array($times) && $allow_past) + { + $next = $times; + } + else + { + if ($this->debug) error_log(__METHOD__."(".array2string($times).", '$id', '$method', ".array2string($data).", $account_id, $debug, $allow_past) returning FALSE"); + return False; + } } if ($account_id === False) { @@ -92,6 +113,7 @@ class Asyncservice ); $this->write($job); + if ($this->debug) error_log(__METHOD__."(".array2string($times).", '$id', '$method', ".array2string($data).", $account_id, $debug, $allow_past) returning TRUE"); return True; } @@ -111,7 +133,7 @@ class Asyncservice { if ($this->debug) { - echo "

next_run("; print_r($times); echo ",'$debug', " . date('Y-m-d H:i', $now) . ")

\n"; + error_log(__METHOD__."(".array2string($times).", '$debug', " . date('Y-m-d H:i', $now) . ")"); $debug = True; // enable syntax-error messages too } if(is_null($now)) { @@ -179,7 +201,7 @@ class Asyncservice foreach($units as $u => $date_pattern) { ++$n; - if ($this->debug) { echo "

n=$n, $u: isset(times[$u]="; print_r($times[$u]); echo ")=".(isset($times[$u])?'True':'False')."

\n"; } + if ($this->debug) error_log("n=$n, $u: isset(times[$u]=".array2string($times[$u]).")=".(isset($times[$u])?'True':'False')); if (isset($times[$u])) { if(is_array($times[$u])) { @@ -197,7 +219,7 @@ class Asyncservice if (count($arr) != 2 || !is_numeric($min) || !is_numeric($max) || $min > $max) { - if ($debug) echo "

Syntax error in $u='$t', allowed is 'min-max', min <= max, min='$min', max='$max'

\n"; + if ($debug) error_log("Syntax error in $u='$t', allowed is 'min-max', min <= max, min='$min', max='$max'"); return False; } @@ -215,7 +237,7 @@ class Asyncservice if (!(is_numeric($one) && count($arr) == 1 || count($arr) == 2 && is_numeric($inc))) { - if ($debug) echo "

Syntax error in $u='$t', allowed is a number or '{*|range}/inc', inc='$inc'

\n"; + if ($debug) error_log("Syntax error in $u='$t', allowed is a number or '{*|range}/inc', inc='$inc'"); return False; } @@ -233,7 +255,7 @@ class Asyncservice } elseif (count($arr) != 2 || $min > $max) { - if ($debug) echo "

Syntax error in $u='$t', allowed is '{*|min-max}/inc', min='$min',max='$max', inc='$inc'

\n"; + if ($debug) error_log("Syntax error in $u='$t', allowed is '{*|min-max}/inc', min='$min',max='$max', inc='$inc'"); return False; } for ($i = $min; $i <= $max; $i += $inc) @@ -256,7 +278,7 @@ class Asyncservice $times[$u][$min_unit[$u]] = True; } } - if ($this->debug) { echo "enumerated times=
"; print_r($times); echo "
\n"; } + if ($this->debug) error_log("enumerated times=".array2string($times)); // now we have the times enumerated, lets find the first not expired one // @@ -274,7 +296,7 @@ class Asyncservice if (isset($found[$u])) { $future = $future || $found[$u] > $unit_now; - if ($this->debug) echo "--> already have a $u = ".$found[$u].", future='$future'
\n"; + if ($this->debug) error_log("--> already have a $u = ".$found[$u].", future='$future'"); continue; // already set } foreach(array_keys($times[$u]) as $unit_value) @@ -304,18 +326,18 @@ class Asyncservice $nexts = array_keys($units); if (!isset($nexts[count($found)-1])) { - if ($this->debug) echo "

Nothing found, exiting !!!

\n"; + if ($this->debug) error_log("Nothing found, exiting !!!"); return False; } $next = $nexts[count($found)-1]; $over = $found[$next]; unset($found[$next]); - if ($this->debug) echo "

Have to try the next $next, $u's are over for $next=$over !!!

\n"; + if ($this->debug) error_log("Have to try the next $next, $u's are over for $next=$over !!!"); break; } } } - if ($this->debug) { echo "

next="; print_r($found); echo "

\n"; } + if ($this->debug) error_log("next=".array2string($found)); return mktime($found['hour'],$found['min'],0,$found['month'],$found['day'],$found['year']); } @@ -377,7 +399,7 @@ class Asyncservice ); // as the async_next column is used as a semaphore we only update it, // if it is 0 (semaphore released) or older then 10min to recover from failed or crashed attempts - if ($exists) $where = array('async_next=0 OR async_next<'.(time()-600)); + if ($exists) $where = array('(async_next=0 OR async_next<'.(time()-600).')'); } //echo "last_run=
"; print_r($last_run); echo "
\n"; return $this->write($last_run, !!$exists, $where) > 0; @@ -442,9 +464,31 @@ class Asyncservice { $job['data'] += array_diff_key($job,array('data' => false)); } - // update job before running it, to cope with jobs taking longer then async-frequency - if (($job['next'] = $this->next_run($job['times']))) + + if ($this->debug) error_log(__METHOD__."() processing job ".array2string($job)); + + // purge keeped jobs, if they are due + if (is_array($job['data']) && !empty($job['data']['keep_time'])) { + if ($job['data']['keep_time'] <= time()) + { + error_log(__METHOD__."() finally deleting job ".array2string($job)); + $this->delete($job['id']); + } + if ($this->debug) error_log(__METHOD__."() keeping job ".array2string($job)); + continue; + } + + // update job before running it, to cope with jobs taking longer then async-frequency + if (($job['next'] = $this->next_run($job['times'])) || + // keep jobs with keep set (eg. calendar alarms) around + is_array($job['data']) && !empty($job['data']['keep'])) + { + if (!$job['next']) + { + $job['next'] = $job['data']['keep_time'] = time() + ((int)$job['data']['keep'] > 1 ? $job['data']['keep'] : self::DEFAULT_KEEP_TIME); + if ($this->debug) error_log(__METHOD__."() setting keep_time to ".date('Y-m-d H:i:s', $job['data']['keep_time']).' for job '.array2string($job)); + } $this->write($job, True); } else // no further runs @@ -453,6 +497,7 @@ class Asyncservice } try { + if ($this->debug) error_log(__METHOD__."() running job ".array2string($job)); ExecMethod($job['method'],$job['data']); } catch(Exception $e) @@ -562,6 +607,7 @@ class Asyncservice */ function delete($id) { + if ($this->debug) error_log(__METHOD__."('$id') ".function_backtrace()); $this->db->delete($this->db_table,array('async_id' => $id),__LINE__,__FILE__); return $this->db->affected_rows(); @@ -659,7 +705,7 @@ class Asyncservice $n = 0; while ($line = fgets($crontab,256)) { - if ($this->debug) echo 'line '.++$n.": $line
\n"; + if ($this->debug) error_log(__METHOD__.'() line '.++$n.": $line"); $parts = explode(' ',$line,6); if ($line{0} == '#' || count($parts) < 6 || ($parts[5]{0} != '/' && substr($parts[5],0,3) != 'php')) diff --git a/calendar/inc/class.calendar_so.inc.php b/calendar/inc/class.calendar_so.inc.php index 94e82ce1d0..d9f43dd0d9 100644 --- a/calendar/inc/class.calendar_so.inc.php +++ b/calendar/inc/class.calendar_so.inc.php @@ -120,6 +120,11 @@ class calendar_so */ const STATUS_SORT = "CASE cal_status WHEN 'U' THEN 1 WHEN 'T' THEN 2 WHEN 'A' THEN 3 WHEN 'R' THEN 4 ELSE 0 END ASC"; + /** + * Time to keep alarms in async table to allow eg. alarm snozzing + */ + const ALARM_KEEP_TIME = 86400; + /** * Cached timezone data * @@ -2400,11 +2405,18 @@ ORDER BY cal_user_type, cal_usre_id $this->async->cancel_timer($id); } $alarm['cal_id'] = $cal_id; // we need the back-reference + // do not deleted async-job, as we need it for alarm snozzing + $alarm['keep'] = self::ALARM_KEEP_TIME; + // past alarms need NOT to be triggered, but kept around for a while to allow alarm snozzing + if ($alarm['time'] < time()) + { + $alarm['time'] = $alarm['keep_time'] = time()+self::ALARM_KEEP_TIME; + } // add an alarm uid, if none is given if (empty($alarm['uid']) && class_exists('Horde_Support_Uuid')) $alarm['uid'] = (string)new Horde_Support_Uuid; //error_log(__METHOD__.__LINE__.' Save Alarm for CalID:'.$cal_id.'->'.array2string($alarm).'-->'.$id.'#'.function_backtrace()); // allways store job with the alarm owner as job-owner to get eg. the correct from address - if (!$this->async->set_timer($alarm['time'],$id,'calendar.calendar_boupdate.send_alarm',$alarm,$alarm['owner'])) + if (!$this->async->set_timer($alarm['time'], $id, 'calendar.calendar_boupdate.send_alarm', $alarm, $alarm['owner'], false, true)) { return False; }