* 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-<timestampt>
This commit is contained in:
Ralf Becker 2018-08-07 15:02:31 +02:00
parent 77aa6cae4e
commit af6c2a0f25
2 changed files with 78 additions and 20 deletions

View File

@ -9,7 +9,6 @@
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package api * @package api
* @access public * @access public
* @version $Id$
*/ */
namespace EGroupware\Api; namespace EGroupware\Api;
@ -30,12 +29,22 @@ class Asyncservice
*/ */
var $db; var $db;
var $db_table = 'egw_async'; 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 * Line in crontab set by constructor with absolute path
*/ */
var $cronline = '/api/asyncservices.php default'; 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 * 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. * 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. * 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') * 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. * for every 5mins in the time from 9am to 5pm.
@ -67,16 +76,28 @@ class Asyncservice
* '<app>.<class>.<public function>'. * '<app>.<class>.<public function>'.
* @param mixed $data =null This data is passed back when the method is called. If it is an array, * @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, ... * 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 int $account_id account_id, under which the methode should be called or False for the actual user
* @param boolean $debug =false * @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 * @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) || if (empty($id) || empty($method) || $this->read($id) ||
!($next = $this->next_run($times,$debug))) !($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) if ($account_id === False)
{ {
@ -92,6 +113,7 @@ class Asyncservice
); );
$this->write($job); $this->write($job);
if ($this->debug) error_log(__METHOD__."(".array2string($times).", '$id', '$method', ".array2string($data).", $account_id, $debug, $allow_past) returning TRUE");
return True; return True;
} }
@ -111,7 +133,7 @@ class Asyncservice
{ {
if ($this->debug) if ($this->debug)
{ {
echo "<p>next_run("; print_r($times); echo ",'$debug', " . date('Y-m-d H:i', $now) . ")</p>\n"; error_log(__METHOD__."(".array2string($times).", '$debug', " . date('Y-m-d H:i', $now) . ")");
$debug = True; // enable syntax-error messages too $debug = True; // enable syntax-error messages too
} }
if(is_null($now)) { if(is_null($now)) {
@ -179,7 +201,7 @@ class Asyncservice
foreach($units as $u => $date_pattern) foreach($units as $u => $date_pattern)
{ {
++$n; ++$n;
if ($this->debug) { echo "<p>n=$n, $u: isset(times[$u]="; print_r($times[$u]); echo ")=".(isset($times[$u])?'True':'False')."</p>\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 (isset($times[$u]))
{ {
if(is_array($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 (count($arr) != 2 || !is_numeric($min) || !is_numeric($max) || $min > $max)
{ {
if ($debug) echo "<p>Syntax error in $u='$t', allowed is 'min-max', min <= max, min='$min', max='$max'</p>\n"; if ($debug) error_log("Syntax error in $u='$t', allowed is 'min-max', min <= max, min='$min', max='$max'");
return False; return False;
} }
@ -215,7 +237,7 @@ class Asyncservice
if (!(is_numeric($one) && count($arr) == 1 || if (!(is_numeric($one) && count($arr) == 1 ||
count($arr) == 2 && is_numeric($inc))) count($arr) == 2 && is_numeric($inc)))
{ {
if ($debug) echo "<p>Syntax error in $u='$t', allowed is a number or '{*|range}/inc', inc='$inc'</p>\n"; if ($debug) error_log("Syntax error in $u='$t', allowed is a number or '{*|range}/inc', inc='$inc'");
return False; return False;
} }
@ -233,7 +255,7 @@ class Asyncservice
} }
elseif (count($arr) != 2 || $min > $max) elseif (count($arr) != 2 || $min > $max)
{ {
if ($debug) echo "<p>Syntax error in $u='$t', allowed is '{*|min-max}/inc', min='$min',max='$max', inc='$inc'</p>\n"; if ($debug) error_log("Syntax error in $u='$t', allowed is '{*|min-max}/inc', min='$min',max='$max', inc='$inc'");
return False; return False;
} }
for ($i = $min; $i <= $max; $i += $inc) for ($i = $min; $i <= $max; $i += $inc)
@ -256,7 +278,7 @@ class Asyncservice
$times[$u][$min_unit[$u]] = True; $times[$u][$min_unit[$u]] = True;
} }
} }
if ($this->debug) { echo "enumerated times=<pre>"; print_r($times); echo "</pre>\n"; } if ($this->debug) error_log("enumerated times=".array2string($times));
// now we have the times enumerated, lets find the first not expired one // now we have the times enumerated, lets find the first not expired one
// //
@ -274,7 +296,7 @@ class Asyncservice
if (isset($found[$u])) if (isset($found[$u]))
{ {
$future = $future || $found[$u] > $unit_now; $future = $future || $found[$u] > $unit_now;
if ($this->debug) echo "--> already have a $u = ".$found[$u].", future='$future'<br>\n"; if ($this->debug) error_log("--> already have a $u = ".$found[$u].", future='$future'");
continue; // already set continue; // already set
} }
foreach(array_keys($times[$u]) as $unit_value) foreach(array_keys($times[$u]) as $unit_value)
@ -304,18 +326,18 @@ class Asyncservice
$nexts = array_keys($units); $nexts = array_keys($units);
if (!isset($nexts[count($found)-1])) if (!isset($nexts[count($found)-1]))
{ {
if ($this->debug) echo "<p>Nothing found, exiting !!!</p>\n"; if ($this->debug) error_log("Nothing found, exiting !!!");
return False; return False;
} }
$next = $nexts[count($found)-1]; $next = $nexts[count($found)-1];
$over = $found[$next]; $over = $found[$next];
unset($found[$next]); unset($found[$next]);
if ($this->debug) echo "<p>Have to try the next $next, $u's are over for $next=$over !!!</p>\n"; if ($this->debug) error_log("Have to try the next $next, $u's are over for $next=$over !!!");
break; break;
} }
} }
} }
if ($this->debug) { echo "<p>next="; print_r($found); echo "</p>\n"; } if ($this->debug) error_log("next=".array2string($found));
return mktime($found['hour'],$found['min'],0,$found['month'],$found['day'],$found['year']); 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, // 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 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=<pre>"; print_r($last_run); echo "</pre>\n"; //echo "last_run=<pre>"; print_r($last_run); echo "</pre>\n";
return $this->write($last_run, !!$exists, $where) > 0; return $this->write($last_run, !!$exists, $where) > 0;
@ -442,9 +464,31 @@ class Asyncservice
{ {
$job['data'] += array_diff_key($job,array('data' => false)); $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); $this->write($job, True);
} }
else // no further runs else // no further runs
@ -453,6 +497,7 @@ class Asyncservice
} }
try try
{ {
if ($this->debug) error_log(__METHOD__."() running job ".array2string($job));
ExecMethod($job['method'],$job['data']); ExecMethod($job['method'],$job['data']);
} }
catch(Exception $e) catch(Exception $e)
@ -562,6 +607,7 @@ class Asyncservice
*/ */
function delete($id) function delete($id)
{ {
if ($this->debug) error_log(__METHOD__."('$id') ".function_backtrace());
$this->db->delete($this->db_table,array('async_id' => $id),__LINE__,__FILE__); $this->db->delete($this->db_table,array('async_id' => $id),__LINE__,__FILE__);
return $this->db->affected_rows(); return $this->db->affected_rows();
@ -659,7 +705,7 @@ class Asyncservice
$n = 0; $n = 0;
while ($line = fgets($crontab,256)) while ($line = fgets($crontab,256))
{ {
if ($this->debug) echo 'line '.++$n.": $line<br>\n"; if ($this->debug) error_log(__METHOD__.'() line '.++$n.": $line");
$parts = explode(' ',$line,6); $parts = explode(' ',$line,6);
if ($line{0} == '#' || count($parts) < 6 || ($parts[5]{0} != '/' && substr($parts[5],0,3) != 'php')) if ($line{0} == '#' || count($parts) < 6 || ($parts[5]{0} != '/' && substr($parts[5],0,3) != 'php'))

View File

@ -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"; 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 * Cached timezone data
* *
@ -2400,11 +2405,18 @@ ORDER BY cal_user_type, cal_usre_id
$this->async->cancel_timer($id); $this->async->cancel_timer($id);
} }
$alarm['cal_id'] = $cal_id; // we need the back-reference $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 // add an alarm uid, if none is given
if (empty($alarm['uid']) && class_exists('Horde_Support_Uuid')) $alarm['uid'] = (string)new Horde_Support_Uuid; 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()); //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 // 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; return False;
} }