WIP: periodic running admin-commands

This commit is contained in:
Ralf Becker 2018-09-18 16:24:09 +02:00
parent cdae6c4b01
commit c1316beda5
4 changed files with 300 additions and 175 deletions

View File

@ -45,6 +45,10 @@ use EGroupware\Api\Acl;
* foreign key into egw_admin_remote (table of remote systems administrated by this one)
* @property-read int $account account_id of user affected by this cmd or NULL
* @property-read string $app app-name affected by this cmd or NULL
* @property-read string $parent parent cmd (with rrule) of single periodic execution
* @property-read string $rrule rrule for periodic execution
* @property int $rrule_start optional start timestamp for rrule, default $created time
* @property string async_job_id optional name of async job for periodic-run, default "admin-cmd-$id"
*/
abstract class admin_cmd
{
@ -67,7 +71,7 @@ abstract class admin_cmd
*
* @var int
*/
protected $status;
protected $status = self::successful;
static $stati = array(
admin_cmd::scheduled => 'scheduled',
@ -96,6 +100,8 @@ abstract class admin_cmd
public $remote_id;
protected $account;
protected $app;
protected $rrule;
protected $parent;
/**
* Display name of command, default ucfirst(str_replace(['_cmd_', '_'], ' ', __CLASS__))
@ -387,6 +393,16 @@ abstract class admin_cmd
{
admin_cmd::_set_async_job();
}
// schedule periodic execution, if we have an rrule
elseif (!empty($this->rrule))
{
$this->set_periodic_job();
}
// existing object with no rrule, cancle evtl. running periodic job
elseif($vars['id'])
{
$this->cancel_periodic_job();
}
return true;
}
@ -418,12 +434,12 @@ abstract class admin_cmd
}
/**
* reading a command from the queue returning the comand object
* Reading a command from the queue returning the comand object
*
* @static
* @param int|string $id id or uid of the command
* @return admin_cmd or null if record not found
* @throws Exception(lang('Unknown command %1!',$class),0);
* @throws Api\Exception\WrongParameter if class does not exist or is no instance of admin_cmd
*/
static function read($id)
{
@ -977,6 +993,72 @@ abstract class admin_cmd
return admin_cmd::_set_async_job();
}
const PERIOD_ASYNC_ID_PREFIX = 'admin-cmd-';
/**
* Schedule next execution of a periodic job
*
* @return boolean
*/
public function set_periodic_job()
{
if (empty($this->rrule)) return false;
// parse rrule and calculate next execution time
$event = calendar_rrule::parseRrule($this->rrule, true); // true: allow HOURLY or MINUTELY
// rrule can depend on start-time, use policy creation time by default, if rrule_start is not set
$event['start'] = empty($this->rrule_start) ? $this->created : $this->rrule_start;
$event['tzid'] = Api\DateTime::$server_timezone->getName();
$rrule = calendar_rrule::event2rrule($event, false); // false = server timezone
$rrule->rewind();
while((($time = $rrule->current()->format('ts'))) <= time())
{
$rrule->next();
}
// schedule run_periodic_job to run at that time
$async = new Api\Asyncservice();
$job_id = empty($this->async_job_id) ? self::PERIOD_ASYNC_ID_PREFIX.$this->id : $this->async_job_id;
$async->cancel_timer($job_id); // we delete it in case a job already exists
return $async->set_timer($time, $job_id, __CLASS__.'::run_periodic_job', $this->as_array(), $this->creator);
}
/**
* Cancel evtl. existing periodic job
*
* @return boolean true if job was canceled, false otherwise
*/
protected function cancel_periodic_job()
{
$async = new Api\Asyncservice();
$job_id = empty($this->async_job_id) ? self::PERIOD_ASYNC_ID_PREFIX.$this->id : $this->async_job_id;
$async->cancel_timer($job_id); // we delete it in case a job already exists
}
/**
* Run a periodic job, record it's result and schedule next run
*/
static function run_periodic_job($data)
{
$cmd = admin_cmd::read($data['id']);
// schedule next execution
$cmd->set_periodic_job();
// instanciate single periodic execution object
$single = $cmd->as_array();
$single['parent'] = $single['id'];
unset($single['id'], $single['uid'], $single['rrule'], $single['created'], $single['modified'], $single['modifier']);
$periodic = admin_cmd::instanciate($single);
try {
$periodic->run(null, false);
}
catch (Exception $ex) {
error_log(__METHOD__."(".array2string($data).") periodic execution failed: ".$ex->getMessage());
}
}
/**
* Return a list of defined remote instances
*

View File

@ -48,7 +48,8 @@ class admin_cmds
$row['title'] = $e->getMessage();
}
$row['data'] = !($data = json_php_unserialize($row['data'])) ? '' :
json_encode($data, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
json_encode($data+(empty($row['rrule'])?array():array('rrule' => $row['rrule'])),
JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
if ($row['status'] == admin_cmd::scheduled)
{

View File

@ -9,7 +9,6 @@
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package calendar
* @subpackage export
* @version $Id$
*/
use EGroupware\Api;
@ -28,15 +27,6 @@ class calendar_ical extends calendar_boupdate
*/
var $supportedFields;
var $recur_days_1_0 = array(
MCAL_M_MONDAY => 'MO',
MCAL_M_TUESDAY => 'TU',
MCAL_M_WEDNESDAY => 'WE',
MCAL_M_THURSDAY => 'TH',
MCAL_M_FRIDAY => 'FR',
MCAL_M_SATURDAY => 'SA',
MCAL_M_SUNDAY => 'SU',
);
/**
* @var array $status_egw2ical conversation of the participant status egw => ical
*/
@ -2640,160 +2630,8 @@ class calendar_ical extends calendar_boupdate
$vcardData['location'] = str_replace("\r\n", "\n", $attributes['value']);
break;
case 'RRULE':
$recurence = $attributes['value'];
$vcardData['recur_interval'] = 1;
$type = preg_match('/FREQ=([^;: ]+)/i',$recurence,$matches) ? $matches[1] : $recurence[0];
// vCard 2.0 values for all types
if (preg_match('/UNTIL=([0-9TZ]+)/',$recurence,$matches))
{
$vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($matches[1]);
// If it couldn't be parsed, treat it as not set
if(is_string($vcardData['recur_enddate']))
{
unset($vcardData['recur_enddate']);
}
else
{
// iCal defines enddate to be a time and eg. Apple sends 1s less then next recurance, if they split events
self::check_fix_endate($vcardData);
}
}
elseif (preg_match('/COUNT=([0-9]+)/',$recurence,$matches))
{
$vcardData['recur_count'] = (int)$matches[1];
}
if (preg_match('/INTERVAL=([0-9]+)/',$recurence,$matches))
{
$vcardData['recur_interval'] = (int) $matches[1] ? (int) $matches[1] : 1;
}
$vcardData['recur_data'] = 0;
switch($type)
{
case 'D': // 1.0
$recurenceMatches = null;
if (preg_match('/D(\d+) #(\d+)/', $recurence, $recurenceMatches))
{
$vcardData['recur_interval'] = $recurenceMatches[1];
$vcardData['recur_count'] = $recurenceMatches[2];
}
elseif (preg_match('/D(\d+) (.*)/', $recurence, $recurenceMatches))
{
$vcardData['recur_interval'] = $recurenceMatches[1];
$vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime(trim($recurenceMatches[2]));
}
else break;
// fall-through
case 'DAILY': // 2.0
$vcardData['recur_type'] = MCAL_RECUR_DAILY;
if (stripos($recurence, 'BYDAY') === false) break;
// hack to handle TYPE=DAILY;BYDAY= as WEEKLY, which is true as long as there's no interval
// fall-through
case 'W':
case 'WEEKLY':
$days = array();
if (preg_match('/W(\d+) *((?i: [AEFHMORSTUW]{2})+)?( +([^ ]*))$/',$recurence, $recurenceMatches)) // 1.0
{
$vcardData['recur_interval'] = $recurenceMatches[1];
if (empty($recurenceMatches[2]))
{
$days[0] = strtoupper(substr(date('D', $vcardData['start']),0,2));
}
else
{
$days = explode(' ',trim($recurenceMatches[2]));
}
$repeatMatches = null;
if (preg_match('/#(\d+)/',$recurenceMatches[4],$repeatMatches))
{
if ($repeatMatches[1]) $vcardData['recur_count'] = $repeatMatches[1];
}
else
{
$vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[4]);
}
$recur_days = $this->recur_days_1_0;
}
elseif (preg_match('/BYDAY=([^;: ]+)/',$recurence,$recurenceMatches)) // 2.0
{
$days = explode(',',$recurenceMatches[1]);
$recur_days = $this->recur_days;
}
else // no day given, use the day of dtstart
{
$vcardData['recur_data'] |= 1 << (int)date('w',$vcardData['start']);
$vcardData['recur_type'] = MCAL_RECUR_WEEKLY;
}
if ($days)
{
foreach ($recur_days as $id => $day)
{
if (in_array(strtoupper(substr($day,0,2)),$days))
{
$vcardData['recur_data'] |= $id;
}
}
$vcardData['recur_type'] = MCAL_RECUR_WEEKLY;
}
break;
case 'M':
if (preg_match('/MD(\d+)(?: [^ ]+)? #(\d+)/', $recurence, $recurenceMatches))
{
$vcardData['recur_type'] = MCAL_RECUR_MONTHLY_MDAY;
$vcardData['recur_interval'] = $recurenceMatches[1];
$vcardData['recur_count'] = $recurenceMatches[2];
}
elseif (preg_match('/MD(\d+)(?: [^ ]+)? ([0-9TZ]+)/',$recurence, $recurenceMatches))
{
$vcardData['recur_type'] = MCAL_RECUR_MONTHLY_MDAY;
$vcardData['recur_interval'] = $recurenceMatches[1];
$vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]);
}
elseif (preg_match('/MP(\d+) (.*) (.*) (.*)/',$recurence, $recurenceMatches))
{
$vcardData['recur_type'] = MCAL_RECUR_MONTHLY_WDAY;
$vcardData['recur_interval'] = $recurenceMatches[1];
if (preg_match('/#(\d+)/',$recurenceMatches[4],$recurenceMatches))
{
$vcardData['recur_count'] = $recurenceMatches[1];
}
else
{
$vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime(trim($recurenceMatches[4]));
}
}
break;
case 'Y': // 1.0
if (preg_match('/YM(\d+)(?: [^ ]+)? #(\d+)/', $recurence, $recurenceMatches))
{
$vcardData['recur_interval'] = $recurenceMatches[1];
$vcardData['recur_count'] = $recurenceMatches[2];
}
elseif (preg_match('/YM(\d+)(?: [^ ]+)? ([0-9TZ]+)/',$recurence, $recurenceMatches))
{
$vcardData['recur_interval'] = $recurenceMatches[1];
$vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]);
} else break;
// fall-through
case 'YEARLY': // 2.0
if (strpos($recurence, 'BYDAY') === false)
{
$vcardData['recur_type'] = MCAL_RECUR_YEARLY;
break;
}
// handle FREQ=YEARLY;BYDAY= as FREQ=MONTHLY;BYDAY= with 12*INTERVAL
$vcardData['recur_interval'] = $vcardData['recur_interval'] ?
12*$vcardData['recur_interval'] : 12;
// fall-through
case 'MONTHLY':
// does currently NOT parse BYDAY or BYMONTH, it has to be specified/identical to DTSTART
$vcardData['recur_type'] = strpos($recurence,'BYDAY') !== false ?
MCAL_RECUR_MONTHLY_WDAY : MCAL_RECUR_MONTHLY_MDAY;
break;
}
$vcardData += calendar_rrule::parseRrule($attributes['value']);
if (!empty($vcardData['recur_enddate'])) self::check_fix_endate ($vcardData);
break;
case 'EXDATE': // current Horde_Icalendar returns dates, no timestamps
if ($attributes['values'])

View File

@ -56,6 +56,15 @@ class calendar_rrule implements Iterator
* Yearly recurrance
*/
const YEARLY = 5;
/**
* Hourly recurrance
*/
const HOURLY = 8;
/**
* Minutely recurrance
*/
const MINUTELY = 7;
/**
* Translate recure types to labels
*
@ -67,7 +76,7 @@ class calendar_rrule implements Iterator
self::WEEKLY => 'Weekly',
self::MONTHLY_WDAY => 'Monthly (by day)',
self::MONTHLY_MDAY => 'Monthly (by date)',
self::YEARLY => 'Yearly'
self::YEARLY => 'Yearly',
);
/**
@ -79,6 +88,8 @@ class calendar_rrule implements Iterator
self::MONTHLY_WDAY => 'MONTHLY', // BYDAY={1..7, -1}{MO..SO, last workday}
self::MONTHLY_MDAY => 'MONTHLY', // BYMONHTDAY={1..31, -1 for last day of month}
self::YEARLY => 'YEARLY',
self::HOURLY => 'HOURLY',
self::MINUTELY => 'MINUTELY',
);
/**
@ -250,7 +261,7 @@ class calendar_rrule implements Iterator
$this->time = $time instanceof Api\DateTime ? $time : new Api\DateTime($time);
if (!in_array($type,array(self::NONE, self::DAILY, self::WEEKLY, self::MONTHLY_MDAY, self::MONTHLY_WDAY, self::YEARLY)))
if (!in_array($type,array(self::NONE, self::DAILY, self::WEEKLY, self::MONTHLY_MDAY, self::MONTHLY_WDAY, self::YEARLY, self::HOURLY, self::MINUTELY)))
{
throw new Api\Exception\WrongParameter(__METHOD__."($time,$type,$interval,$enddate,$weekdays,...) type $type is NOT valid!");
}
@ -451,6 +462,14 @@ class calendar_rrule implements Iterator
$this->current->modify($this->interval.' year');
break;
case self::HOURLY:
$this->current->modify($this->interval.' hour');
break;
case self::MINUTELY:
$this->current->modify($this->interval.' minute');
break;
default:
throw new Api\Exception\AssertionFailed(__METHOD__."() invalid type #$this->type !");
}
@ -787,6 +806,7 @@ class calendar_rrule implements Iterator
/**
* Generate a rrule from a string generated by __toString().
*
* @param String $rrule Recurrence rule in string format, as generated by __toString()
* @param DateTime date Optional date to work from, defaults to today
*/
@ -799,16 +819,16 @@ class calendar_rrule implements Iterator
$weekdays = 0;
$exceptions = array();
list($type, $sub, $conditions) = explode(' (', $rrule);
if(!$conditions)
list($type, $sub, $conds) = explode(' (', $rrule);
if(!$conds)
{
$conditions = $sub;
$conds = $sub;
}
else
{
$type .= " ($sub";
}
$conditions = explode(', ', substr($conditions, 0, -1));
$conditions = explode(', ', substr($conds, 0, -1));
foreach(static::$types as $id => $type_name)
{
@ -859,13 +879,14 @@ class calendar_rrule implements Iterator
}
else if ($condition_name == lang('ends'))
{
list($dow, $date) = explode(', ', $value);
list(, $date) = explode(', ', $value);
$enddate = new DateTime($date);
}
}
return new calendar_rrule($time,$type_id,$interval,$enddate,$weekdays,$exceptions);
}
/**
* Get recurrence data (keys 'recur_*') to merge into an event
*
@ -939,6 +960,189 @@ class calendar_rrule implements Iterator
}
}
}
/**
* Parses a DateTime field and returns a unix timestamp. If the
* field cannot be parsed then the original text is returned
* unmodified.
*
* @param string $text The Icalendar datetime field value.
* @param string $tzid =null A timezone identifier.
*
* @return integer A unix timestamp.
*/
private static function parseIcalDateTime($text, $tzid=null)
{
static $vcal = null;
if (!isset($vcal)) $vcal = new Horde_Icalendar;
return $vcal->_parseDateTime($text, $tzid);
}
/**
* Parse an iCal recurrence-rule string
*
* @param type $recurence
* @param bool $support_below_daily =false true: support FREQ=HOURLY|MINUTELY
* @return type
*/
public static function parseRrule($recurence, $support_below_daily=false)
{
$vcardData = array();
$vcardData['recur_interval'] = 1;
$matches = null;
$type = preg_match('/FREQ=([^;: ]+)/i',$recurence,$matches) ? $matches[1] : $recurence[0];
// vCard 2.0 values for all types
if (preg_match('/UNTIL=([0-9TZ]+)/',$recurence,$matches))
{
$vcardData['recur_enddate'] = self::parseIcalDateTime($matches[1]);
// If it couldn't be parsed, treat it as not set
if(is_string($vcardData['recur_enddate']))
{
unset($vcardData['recur_enddate']);
}
}
elseif (preg_match('/COUNT=([0-9]+)/',$recurence,$matches))
{
$vcardData['recur_count'] = (int)$matches[1];
}
if (preg_match('/INTERVAL=([0-9]+)/',$recurence,$matches))
{
$vcardData['recur_interval'] = (int) $matches[1] ? (int) $matches[1] : 1;
}
$vcardData['recur_data'] = 0;
switch($type)
{
case 'D': // 1.0
$recurenceMatches = null;
if (preg_match('/D(\d+) #(\d+)/', $recurence, $recurenceMatches))
{
$vcardData['recur_interval'] = $recurenceMatches[1];
$vcardData['recur_count'] = $recurenceMatches[2];
}
elseif (preg_match('/D(\d+) (.*)/', $recurence, $recurenceMatches))
{
$vcardData['recur_interval'] = $recurenceMatches[1];
$vcardData['recur_enddate'] = self::parseIcalDateTime(trim($recurenceMatches[2]));
}
else break;
// fall-through
case 'DAILY': // 2.0
$vcardData['recur_type'] = self::DAILY;
if (stripos($recurence, 'BYDAY') === false) break;
// hack to handle TYPE=DAILY;BYDAY= as WEEKLY, which is true as long as there's no interval
// fall-through
case 'W':
case 'WEEKLY':
$days = array();
if (preg_match('/W(\d+) *((?i: [AEFHMORSTUW]{2})+)?( +([^ ]*))$/',$recurence, $recurenceMatches)) // 1.0
{
$vcardData['recur_interval'] = $recurenceMatches[1];
if (empty($recurenceMatches[2]))
{
$days[0] = strtoupper(substr(date('D', $vcardData['start']),0,2));
}
else
{
$days = explode(' ',trim($recurenceMatches[2]));
}
$repeatMatches = null;
if (preg_match('/#(\d+)/',$recurenceMatches[4],$repeatMatches))
{
if ($repeatMatches[1]) $vcardData['recur_count'] = $repeatMatches[1];
}
else
{
$vcardData['recur_enddate'] = self::parseIcalDateTime($recurenceMatches[4]);
}
}
elseif (preg_match('/BYDAY=([^;: ]+)/',$recurence,$recurenceMatches)) // 2.0
{
$days = explode(',',$recurenceMatches[1]);
}
else // no day given, use the day of dtstart
{
$vcardData['recur_data'] |= 1 << (int)date('w',$vcardData['start']);
$vcardData['recur_type'] = self::WEEKLY;
}
if ($days)
{
foreach (self::$days as $id => $day)
{
if (in_array(strtoupper(substr($day,0,2)),$days))
{
$vcardData['recur_data'] |= $id;
}
}
$vcardData['recur_type'] = self::WEEKLY;
}
break;
case 'M':
if (preg_match('/MD(\d+)(?: [^ ]+)? #(\d+)/', $recurence, $recurenceMatches))
{
$vcardData['recur_type'] = self::MONTHLY_MDAY;
$vcardData['recur_interval'] = $recurenceMatches[1];
$vcardData['recur_count'] = $recurenceMatches[2];
}
elseif (preg_match('/MD(\d+)(?: [^ ]+)? ([0-9TZ]+)/',$recurence, $recurenceMatches))
{
$vcardData['recur_type'] = self::MONTHLY_MDAY;
$vcardData['recur_interval'] = $recurenceMatches[1];
$vcardData['recur_enddate'] = self::parseIcalDateTime($recurenceMatches[2]);
}
elseif (preg_match('/MP(\d+) (.*) (.*) (.*)/',$recurence, $recurenceMatches))
{
$vcardData['recur_type'] = self::MONTHLY_WDAY;
$vcardData['recur_interval'] = $recurenceMatches[1];
if (preg_match('/#(\d+)/',$recurenceMatches[4],$recurenceMatches))
{
$vcardData['recur_count'] = $recurenceMatches[1];
}
else
{
$vcardData['recur_enddate'] = self::parseIcalDateTime(trim($recurenceMatches[4]));
}
}
break;
case 'Y': // 1.0
if (preg_match('/YM(\d+)(?: [^ ]+)? #(\d+)/', $recurence, $recurenceMatches))
{
$vcardData['recur_interval'] = $recurenceMatches[1];
$vcardData['recur_count'] = $recurenceMatches[2];
}
elseif (preg_match('/YM(\d+)(?: [^ ]+)? ([0-9TZ]+)/',$recurence, $recurenceMatches))
{
$vcardData['recur_interval'] = $recurenceMatches[1];
$vcardData['recur_enddate'] = self::parseIcalDateTime($recurenceMatches[2]);
} else break;
// fall-through
case 'YEARLY': // 2.0
if (strpos($recurence, 'BYDAY') === false)
{
$vcardData['recur_type'] = self::YEARLY;
break;
}
// handle FREQ=YEARLY;BYDAY= as FREQ=MONTHLY;BYDAY= with 12*INTERVAL
$vcardData['recur_interval'] = $vcardData['recur_interval'] ?
12*$vcardData['recur_interval'] : 12;
// fall-through
case 'MONTHLY':
// does currently NOT parse BYDAY or BYMONTH, it has to be specified/identical to DTSTART
$vcardData['recur_type'] = strpos($recurence,'BYDAY') !== false ?
self::MONTHLY_WDAY : self::MONTHLY_MDAY;
break;
case 'HOURLY':
if ($support_below_daily) $vcardData['recur_type'] = self::HOURLY;
break;
case 'MINUTELY':
if ($support_below_daily) $vcardData['recur_type'] = self::MINUTELY;
break;
}
return $vcardData;
}
}
if (isset($_SERVER['SCRIPT_FILENAME']) && $_SERVER['SCRIPT_FILENAME'] == __FILE__) // some tests