Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 124 additions & 18 deletions core/class/calendar_event.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ class calendar_event {
private $until = null;
private $_changed = false;

/* Garde-fou : nombre maximum d'itérations dans la boucle de calcul des
occurrences, pour qu'un agenda mal configuré ne bloque pas le rendu. */
const MAX_OCCURRENCE_LOOP = 100000;

public static function sortEventDate($a, $b) {
if (strtotime($a['start']) == strtotime($b['start'])) {
return 0;
Expand Down Expand Up @@ -159,6 +163,64 @@ public static function getNationalDay($year = null) {
return $holidays;
}

/**
* Avance le couple (startDate, endDate) de N cycles complets pour se placer
* juste avant le début de la plage demandée, sans itérer cycle par cycle.
*
* Le saut est toujours un multiple exact de la fréquence de répétition : la
* grille des occurrences est donc strictement identique à celle obtenue par
* itération. On ne saute que des occurrences dont la fin est antérieure au
* début de la plage, c'est-à-dire celles que la boucle aurait de toute façon
* filtrées. Si le saut calculé dépasse, il est divisé par deux jusqu'à
* devenir sûr, sinon il est abandonné.
*
* Volontairement limité aux unités de durée fixe (minutes/heures/jours/
* semaines) : pour mois/années le nombre d'itérations reste faible et
* l'arithmétique PHP sur les fins de mois n'est pas associative
* (31 janvier +1 mois +1 mois != 31 janvier +2 mois).
*
* @return array array($startDate, $endDate)
*/
private static function fastForwardToRange($_startDate, $_endDate, $_repeat, $_rangeStartTime, $_initStartTime, $_initEndTime) {
$unitSeconds = array(
'minute' => 60, 'minutes' => 60,
'hour' => 3600, 'hours' => 3600,
'day' => 86400, 'days' => 86400,
'week' => 604800, 'weeks' => 604800,
);
if (!isset($_repeat['unite']) || !isset($unitSeconds[$_repeat['unite']])) {
return array($_startDate, $_endDate);
}
$freq = intval(isset($_repeat['freq']) ? $_repeat['freq'] : 0);
if ($freq <= 0) {
return array($_startDate, $_endDate);
}
$step = $freq * $unitSeconds[$_repeat['unite']];
$delta = $_rangeStartTime - strtotime($_endDate);
if ($delta <= ($step * 2)) {
return array($_startDate, $_endDate);
}
$isTimeUnit = ($unitSeconds[$_repeat['unite']] < 86400);
$steps = intval(floor($delta / $step)) - 1;
while ($steps > 0) {
$jump = '+' . ($steps * $freq) . ' ' . $_repeat['unite'] . ' ';
if ($isTimeUnit) {
$newStartTime = strtotime($jump . $_startDate);
$newEndTime = strtotime($jump . $_endDate);
} else {
$newStartTime = strtotime($jump . substr($_startDate, 0, 10) . ' ' . $_initStartTime);
$newEndTime = strtotime($jump . substr($_endDate, 0, 10) . ' ' . $_initEndTime);
}
if ($newStartTime !== false && $newEndTime !== false
&& $newEndTime <= $_rangeStartTime
&& $newStartTime >= strtotime($_startDate)) {
return array(date('Y-m-d H:i:s', $newStartTime), date('Y-m-d H:i:s', $newEndTime));
}
$steps = intval(floor($steps / 2));
}
return array($_startDate, $_endDate);
}

public function getLinkData(&$_data = array('node' => array(), 'link' => array()), $_level = 0, $_drill = 3) {
if (isset($_data['node']['calendar' . $this->getId()])) {
return;
Expand Down Expand Up @@ -255,6 +317,8 @@ public function calculOccurrence($_startDate, $_endDate, $_max = 9999999999, $_r
$return = array();
$repeat = $this->getRepeat();
if (isset($repeat['enable']) && $repeat['enable'] == 1) {
// Dates exclues stockées en clés du tableau : test en O(1) via isset()
// au lieu d'un in_array() sur toute la liste à chaque itération.
$excludeDate = array();
if (isset($repeat['excludeDate']) && $repeat['excludeDate'] != '') {
$excludeDate_tmp = explode(',', $repeat['excludeDate']);
Expand All @@ -265,12 +329,12 @@ public function calculOccurrence($_startDate, $_endDate, $_max = 9999999999, $_r
$startDate = date('Y-m-d', strtotime($expDate[0]));
$endDate = date('Y-m-d', strtotime($expDate[1]));
while (strtotime($startDate) <= strtotime($endDate)) {
$excludeDate[] = $startDate;
$excludeDate[$startDate] = true;
$startDate = date('Y-m-d', strtotime('+1 day ' . $startDate));
}
}
} else {
$excludeDate[] = date('Y-m-d', strtotime($date));
$excludeDate[date('Y-m-d', strtotime($date))] = true;
}
}
}
Expand All @@ -290,10 +354,10 @@ public function calculOccurrence($_startDate, $_endDate, $_max = 9999999999, $_r
$startDate = date('Y-m-d', strtotime($occurrence['start']));
$endDate = date('Y-m-d', strtotime($occurrence['end']));
if ($startDate == $endDate) {
$excludeDate[] = $startDate;
$excludeDate[$startDate] = true;
} else {
while (strtotime($startDate) <= strtotime($endDate)) {
$excludeDate[] = $startDate;
$excludeDate[$startDate] = true;
$startDate = date('Y-m-d', strtotime('+1 day ' . $startDate));
}
}
Expand All @@ -315,39 +379,66 @@ public function calculOccurrence($_startDate, $_endDate, $_max = 9999999999, $_r
// }
$initStartTime = date('H:i:s', strtotime($startDate));
$initEndTime = date('H:i:s', strtotime($endDate));
while ($this->getUntil() == null || strtotime($this->getUntil()) > strtotime($startDate) || $this->getUntil() == '0000-00-00 00:00:00') {
if (!in_array(date('Y-m-d', strtotime($startDate)), $excludeDate) && ($startTime < strtotime($startDate) || strtotime($endDate) > $startTime)) {
if ($repeat['excludeDay'][date('N', strtotime($startDate))] == 1 || (isset($repeat['mode']) && $repeat['mode'] == 'advance')) {

$isAdvanceMode = (isset($repeat['mode']) && $repeat['mode'] == 'advance');

// PERF : on se place directement au début de la plage demandée au lieu
// de dérouler toutes les occurrences depuis la 1ère (un agenda quotidien
// créé en 2017 faisait ~3000 tours de boucle inutiles à chaque affichage
// de widget).
if (!$isAdvanceMode) {
list($startDate, $endDate) = self::fastForwardToRange($startDate, $endDate, $repeat, $startTime, $initStartTime, $initEndTime);
}

// Sorties de la boucle : ces valeurs ne changent jamais d'un tour à
// l'autre et coûtaient plusieurs strtotime() par itération. Au passage,
// strtotime() n'est plus jamais appelé sur un until null.
$until = $this->getUntil();
$hasUntil = ($until !== null && $until != '' && $until != '0000-00-00 00:00:00');
$untilTime = $hasUntil ? strtotime($until) : null;

$nationalDayCache = array();
$loop = 0;
$curStartTime = strtotime($startDate);
$curEndTime = strtotime($endDate);

while (!$hasUntil || $untilTime > $curStartTime) {
if (++$loop > self::MAX_OCCURRENCE_LOOP) {
log::add('calendar', 'debug', __('Calcul des occurrences interrompu (trop d\'itérations) pour l\'évènement', __FILE__) . ' : ' . $this->getId());
break;
}
if (!isset($excludeDate[date('Y-m-d', $curStartTime)]) && ($startTime < $curStartTime || $curEndTime > $startTime)) {
if ((isset($repeat['excludeDay'][date('N', $curStartTime)]) && $repeat['excludeDay'][date('N', $curStartTime)] == 1) || $isAdvanceMode) {
if (!isset($repeat['nationalDay']) || $repeat['nationalDay'] == 'all') {
$return[] = array(
'start' => $startDate,
'end' => $endDate,
);
} else if ($repeat['nationalDay'] == 'exeptNationalDay') {
$nationalDay = self::getNationalDay(date('Y'), strtotime($startDate));
if (!in_array(date('Y-m-d', strtotime($startDate)), $nationalDay)) {
$nationalDay = $this->getNationalDayCached($nationalDayCache, $curStartTime);
if (!in_array(date('Y-m-d', $curStartTime), $nationalDay)) {
$return[] = array(
'start' => $startDate,
'end' => $endDate,
);
}
} else if ($repeat['nationalDay'] == 'onlyNationalDay') {
$nationalDay = self::getNationalDay(date('Y'), strtotime($startDate));
if (in_array(date('Y-m-d', strtotime($startDate)), $nationalDay)) {
$nationalDay = $this->getNationalDayCached($nationalDayCache, $curStartTime);
if (in_array(date('Y-m-d', $curStartTime), $nationalDay)) {
$return[] = array(
'start' => $startDate,
'end' => $endDate,
);
}
} else if ($repeat['nationalDay'] == 'onlyEven') {
if ((date('W', strtotime($startDate)) % 2) == 0) {
if ((date('W', $curStartTime) % 2) == 0) {
$return[] = array(
'start' => $startDate,
'end' => $endDate,
);
}
} else if ($repeat['nationalDay'] == 'onlyOdd') {
if ((date('W', strtotime($startDate)) % 2) == 1) {
if ((date('W', $curStartTime) % 2) == 1) {
$return[] = array(
'start' => $startDate,
'end' => $endDate,
Expand All @@ -359,8 +450,8 @@ public function calculOccurrence($_startDate, $_endDate, $_max = 9999999999, $_r
}
}
}
$prevStartDate = $startDate;
if (isset($repeat['mode']) && $repeat['mode'] == 'advance') {
$prevStartTime = $curStartTime;
if ($isAdvanceMode) {
$nextMonth = date('F', strtotime('+1 month ' . $startDate));
$year = date('Y', strtotime('+1 month ' . $startDate));
$tmp_startDate = date('Y-m-d', strtotime($repeat['positionAt'] . ' ' . $repeat['day'] . ' of ' . $nextMonth . ' ' . $year));
Expand All @@ -370,7 +461,7 @@ public function calculOccurrence($_startDate, $_endDate, $_max = 9999999999, $_r
$endDate = date('Y-m-d H:i:s', strtotime($tmp_startDate . ' ' . $initEndTime));
$startDate = date('Y-m-d H:i:s', strtotime($tmp_startDate . ' ' . $initStartTime));
} else {
if ($repeat['freq'] == 0) {
if (!isset($repeat['freq']) || $repeat['freq'] == 0) {
break;
}
if ($repeat['unite'] == 'hours') {
Expand All @@ -381,10 +472,12 @@ public function calculOccurrence($_startDate, $_endDate, $_max = 9999999999, $_r
$endDate = date('Y-m-d H:i:s', strtotime('+' . $repeat['freq'] . ' ' . $repeat['unite'] . ' ' . substr($endDate, 0, 10) . ' ' . $initEndTime));
}
}
if (strtotime($startDate) <= strtotime($prevStartDate)) {
$curStartTime = strtotime($startDate);
$curEndTime = strtotime($endDate);
if ($curStartTime <= $prevStartTime) {
break;
}
if (strtotime($startDate) > $endTime) {
if ($curStartTime > $endTime) {
break;
}
}
Expand Down Expand Up @@ -482,6 +575,19 @@ public function calculOccurrence($_startDate, $_endDate, $_max = 9999999999, $_r
return $return;
}

/**
* Jours fériés de l'année de l'occurrence, mis en cache.
* easter_date() + 20 mktime() étaient recalculés à chaque itération, et pour
* l'année courante seulement (donc faux pour une occurrence en N+2).
*/
private function getNationalDayCached(&$_cache, $_timestamp) {
$year = intval(date('Y', $_timestamp));
if (!isset($_cache[$year])) {
$_cache[$year] = self::getNationalDay($year);
}
return $_cache[$year];
}

public function preSave() {
if ($this->getEqLogic_id() == '') {
throw new Exception(__("L'id de l'équipement ne peut être vide", __FILE__));
Expand Down