diff options
Diffstat (limited to '')
8 files changed, 1542 insertions, 0 deletions
diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement.php b/vendor/ipl/web/src/FormElement/ScheduleElement.php new file mode 100644 index 0000000..f872f49 --- /dev/null +++ b/vendor/ipl/web/src/FormElement/ScheduleElement.php @@ -0,0 +1,636 @@ +<?php + +namespace ipl\Web\FormElement; + +use DateTime; +use InvalidArgumentException; +use ipl\Html\Attributes; +use ipl\Html\FormElement\FieldsetElement; +use ipl\Html\HtmlElement; +use ipl\Scheduler\Contract\Frequency; +use ipl\Scheduler\Cron; +use ipl\Scheduler\OneOff; +use ipl\Scheduler\RRule; +use ipl\Validator\BetweenValidator; +use ipl\Validator\CallbackValidator; +use ipl\Web\FormElement\ScheduleElement\AnnuallyFields; +use ipl\Web\FormElement\ScheduleElement\Common\FieldsProtector; +use ipl\Web\FormElement\ScheduleElement\MonthlyFields; +use ipl\Web\FormElement\ScheduleElement\Recurrence; +use ipl\Web\FormElement\ScheduleElement\WeeklyFields; +use LogicException; +use Psr\Http\Message\RequestInterface; + +class ScheduleElement extends FieldsetElement +{ + use FieldsProtector; + + /** @var string Plain cron expressions */ + protected const CRON_EXPR = 'cron_expr'; + + /** @var string Configure the individual expression parts manually */ + protected const CUSTOM_EXPR = 'custom'; + + /** @var string Used to run a one-off task */ + protected const NO_REPEAT = 'none'; + + protected $defaultAttributes = ['class' => 'schedule-element']; + + /** @var array A list of allowed frequencies used to configure custom expressions */ + protected $customFrequencies = []; + + /** @var array */ + protected $advanced = []; + + /** @var array */ + protected $regulars = []; + + /** @var string Schedule frequency of this element */ + protected $frequency = self::NO_REPEAT; + + /** @var string */ + protected $customFrequency; + + /** @var DateTime */ + protected $start; + + /** @var WeeklyFields Weekly parts of this schedule element */ + protected $weeklyField; + + /** @var MonthlyFields Monthly parts of this schedule element */ + protected $monthlyFields; + + /** @var AnnuallyFields Annually parts of this schedule element */ + protected $annuallyFields; + + protected function init(): void + { + $this->start = new DateTime(); + $this->weeklyField = new WeeklyFields('weekly-fields', [ + 'default' => $this->start->format('D'), + 'protector' => function (string $day) { + return $this->protectId($day); + }, + ]); + + $this->monthlyFields = new MonthlyFields('monthly-fields', [ + 'default' => $this->start->format('j'), + 'availableFields' => (int) $this->start->format('t'), + 'protector' => function ($day) { + return $this->protectId($day); + } + ]); + + $this->annuallyFields = new AnnuallyFields('annually-fields', [ + 'default' => $this->start->format('M'), + 'protector' => function ($month) { + return $this->protectId($month); + } + ]); + + + $this->regulars = [ + RRule::MINUTELY => $this->translate('Minutely'), + RRule::HOURLY => $this->translate('Hourly'), + RRule::DAILY => $this->translate('Daily'), + RRule::WEEKLY => $this->translate('Weekly'), + RRule::MONTHLY => $this->translate('Monthly'), + RRule::QUARTERLY => $this->translate('Quarterly'), + RRule::YEARLY => $this->translate('Annually'), + ]; + + $this->customFrequencies = array_slice($this->regulars, 2); + unset($this->customFrequencies[RRule::QUARTERLY]); + + $this->advanced = [ + static::CUSTOM_EXPR => $this->translate('Custom…'), + static::CRON_EXPR => $this->translate('Cron Expression…') + ]; + } + + /** + * Get whether this element is rendering a cron expression + * + * @return bool + */ + public function hasCronExpression(): bool + { + return $this->getFrequency() === static::CRON_EXPR; + } + + /** + * Get the frequency of this element + * + * @return string + */ + public function getFrequency(): string + { + return $this->getPopulatedValue('frequency', $this->frequency); + } + + /** + * Set the custom frequency of this schedule element + * + * @param string $frequency + * + * @return $this + */ + public function setFrequency(string $frequency): self + { + if ( + $frequency !== static::NO_REPEAT + && ! isset($this->regulars[$frequency]) + && ! isset($this->advanced[$frequency]) + ) { + throw new InvalidArgumentException(sprintf('Invalid frequency provided: %s', $frequency)); + } + + $this->frequency = $frequency; + + return $this; + } + + /** + * Get custom frequency of this element + * + * @return ?string + */ + public function getCustomFrequency(): ?string + { + return $this->getValue('custom-frequency', $this->customFrequency); + } + + /** + * Set custom frequency of this element + * + * @param string $frequency + * + * @return $this + */ + public function setCustomFrequency(string $frequency): self + { + if (! isset($this->customFrequencies[$frequency])) { + throw new InvalidArgumentException(sprintf('Invalid custom frequency provided: %s', $frequency)); + } + + $this->customFrequency = $frequency; + + return $this; + } + + /** + * Set start time of the parsed expressions + * + * @param DateTime $start + * + * @return $this + */ + public function setStart(DateTime $start): self + { + $this->start = $start; + + // Forward the start time update to the sub elements as well! + $this->weeklyField->setDefault($start->format('D')); + $this->annuallyFields->setDefault($start->format('M')); + $this->monthlyFields + ->setDefault((int) $start->format('j')) + ->setAvailableFields((int) $start->format('t')); + + return $this; + } + + public function getValue($name = null, $default = null) + { + if ($name !== null || ! $this->hasBeenValidated()) { + return parent::getValue($name, $default); + } + + $frequency = $this->getFrequency(); + $start = parent::getValue('start'); + switch ($frequency) { + case static::NO_REPEAT: + return new OneOff($start); + case static::CRON_EXPR: + $rule = new Cron(parent::getValue('cron_expression')); + + break; + case RRule::MINUTELY: + case RRule::HOURLY: + case RRule::DAILY: + case RRule::WEEKLY: + case RRule::MONTHLY: + case RRule::QUARTERLY: + case RRule::YEARLY: + $rule = RRule::fromFrequency($frequency); + + break; + default: // static::CUSTOM_EXPR + $interval = parent::getValue('interval', 1); + $customFrequency = parent::getValue('custom-frequency', RRule::DAILY); + switch ($customFrequency) { + case RRule::DAILY: + if ($interval === '*') { + $interval = 1; + } + + $rule = new RRule("FREQ=DAILY;INTERVAL=$interval"); + + break; + case RRule::WEEKLY: + $byDay = implode(',', $this->weeklyField->getSelectedWeekDays()); + + $rule = new RRule("FREQ=WEEKLY;INTERVAL=$interval;BYDAY=$byDay"); + + break; + /** @noinspection PhpMissingBreakStatementInspection */ + case RRule::MONTHLY: + $runsOn = $this->monthlyFields->getValue('runsOn', MonthlyFields::RUNS_EACH); + if ($runsOn === MonthlyFields::RUNS_EACH) { + $byMonth = implode(',', $this->monthlyFields->getSelectedDays()); + + $rule = new RRule("FREQ=MONTHLY;INTERVAL=$interval;BYMONTHDAY=$byMonth"); + + break; + } + // Fall-through to the next switch case + case RRule::YEARLY: + $rule = "FREQ=MONTHLY;INTERVAL=$interval;"; + if ($customFrequency === RRule::YEARLY) { + $runsOn = $this->annuallyFields->getValue('runsOnThe', 'n'); + $month = $this->annuallyFields->getValue('month', (int) $this->start->format('m')); + if (is_string($month)) { + $datetime = DateTime::createFromFormat('!M', $month); + if (! $datetime) { + throw new InvalidArgumentException(sprintf('Invalid month provided: %s', $month)); + } + + $month = (int) $datetime->format('m'); + } + + $rule = "FREQ=YEARLY;INTERVAL=1;BYMONTH=$month;"; + if ($runsOn === 'n') { + $rule = new RRule($rule); + + break; + } + } + + $element = $this->monthlyFields; + if ($customFrequency === RRule::YEARLY) { + $element = $this->annuallyFields; + } + + $runDay = $element->getValue('day', $element::$everyDay); + $ordinal = $element->getValue('ordinal', $element::$first); + $position = $element->getOrdinalAsInteger($ordinal); + + if ($runDay === $element::$everyDay) { + $rule .= "BYDAY=MO,TU,WE,TH,FR,SA,SU;BYSETPOS=$position"; + } elseif ($runDay === $element::$everyWeekday) { + $rule .= "BYDAY=MO,TU,WE,TH,FR;BYSETPOS=$position"; + } elseif ($runDay === $element::$everyWeekend) { + $rule .= "BYDAY=SA,SU;BYSETPOS=$position"; + } else { + $rule .= sprintf('BYDAY=%d%s', $position, $runDay); + } + + $rule = new RRule($rule); + + break; + default: + throw new LogicException(sprintf('Custom frequency %s is not supported!', $customFrequency)); + } + } + + $rule->startAt($start); + if (parent::getValue('use-end-time', 'n') === 'y') { + $rule->endAt(parent::getValue('end')); + } + + // Sync the start time and first recurrence of the rule + if (! $this->hasCronExpression() && $this->getFrequency() !== static::NO_REPEAT) { + $nextDue = $rule->getNextRecurrences($start)->current() ?? $start; + $rule->startAt($nextDue); + } + + return $rule; + } + + public function setValue($value) + { + $values = $value; + $rule = $value; + if ($rule instanceof Frequency) { + if ($rule->getStart()) { + $this->setStart($rule->getStart()); + } + + $values = []; + if ($rule->getEnd() && ! $rule instanceof OneOff) { + $values['use-end-time'] = 'y'; + $values['end'] = $rule->getEnd(); + } + + if ($rule instanceof OneOff) { + $values['frequency'] = static::NO_REPEAT; + } elseif ($rule instanceof Cron) { + $values['cron_expression'] = $rule->getExpression(); + $values['frequency'] = static::CRON_EXPR; + + $this->setFrequency(static::CRON_EXPR); + } elseif ($rule instanceof RRule) { + $values['interval'] = $rule->getInterval(); + switch ($rule->getFrequency()) { + case RRule::DAILY: + if ($rule->getInterval() <= 1 && strpos($rule->getString(), 'INTERVAL=') === false) { + $this->setFrequency(RRule::DAILY); + } else { + $this + ->setFrequency(static::CUSTOM_EXPR) + ->setCustomFrequency(RRule::DAILY); + } + + break; + case RRule::WEEKLY: + if (! $rule->getByDay() || empty($rule->getByDay())) { + $this->setFrequency(RRule::WEEKLY); + } else { + $values['weekly-fields'] = $this->weeklyField->loadWeekDays($rule->getByDay()); + $this + ->setFrequency(static::CUSTOM_EXPR) + ->setCustomFrequency(RRule::WEEKLY); + } + + break; + case RRule::MONTHLY: + case RRule::YEARLY: + $isMonthly = $rule->getFrequency() === RRule::MONTHLY; + if ($rule->getByDay() || $rule->getByMonthDay() || $rule->getByMonth()) { + $this->setFrequency(static::CUSTOM_EXPR); + + if ($isMonthly) { + $values['monthly-fields'] = $this->monthlyFields->loadRRule($rule); + $this->setCustomFrequency(RRule::MONTHLY); + } else { + $values['annually-fields'] = $this->annuallyFields->loadRRule($rule); + $this->setCustomFrequency(RRule::YEARLY); + } + } elseif ($isMonthly && $rule->getInterval() === 3) { + $this->setFrequency(RRule::QUARTERLY); + } else { + $this->setFrequency($rule->getFrequency()); + } + + break; + default: + $this->setFrequency($rule->getFrequency()); + } + + $values['frequency'] = $this->getFrequency(); + $values['custom-frequency'] = $this->getCustomFrequency(); + } + } + + return parent::setValue($values); + } + + protected function assemble() + { + $start = $this->getPopulatedValue('start') ?: $this->start; + if (! $start instanceof DateTime) { + $start = new DateTime($start); + } + $this->setStart($start); + + $autosubmit = ! $this->hasCronExpression() && $this->getFrequency() !== static::NO_REPEAT; + $this->addElement('localDateTime', 'start', [ + 'class' => $autosubmit ? 'autosubmit' : null, + 'required' => true, + 'label' => $this->translate('Start'), + 'value' => $start, + 'description' => $this->translate('Start time of this schedule') + ]); + + $this->addElement('checkbox', 'use-end-time', [ + 'required' => false, + 'class' => 'autosubmit', + 'disabled' => $this->getPopulatedValue('frequency', static::NO_REPEAT) === static::NO_REPEAT ?: null, + 'value' => $this->getPopulatedValue('use-end-time', 'n'), + 'label' => $this->translate('Use End Time') + ]); + + if ($this->getPopulatedValue('use-end-time', 'n') === 'y') { + $end = $this->getPopulatedValue('end', new DateTime()); + if (! $end instanceof DateTime) { + $end = new DateTime($end); + } + + $this->addElement('localDateTime', 'end', [ + 'class' => ! $this->hasCronExpression() ? 'autosubmit' : null, + 'required' => true, + 'value' => $end, + 'label' => $this->translate('End'), + 'description' => $this->translate('End time of this schedule') + ]); + } + + $this->addElement('select', 'frequency', [ + 'required' => false, + 'class' => 'autosubmit', + 'label' => $this->translate('Frequency'), + 'description' => $this->translate('Specifies how often this job run should be recurring'), + 'options' => [ + static::NO_REPEAT => $this->translate('None'), + $this->translate('Regular') => $this->regulars, + $this->translate('Advanced') => $this->advanced + ], + ]); + + if ($this->getFrequency() === static::CUSTOM_EXPR) { + $this->addElement('select', 'custom-frequency', [ + 'required' => false, + 'class' => 'autosubmit', + 'value' => parent::getValue('custom-frequency'), + 'options' => $this->customFrequencies, + 'label' => $this->translate('Custom Frequency'), + 'description' => $this->translate('Specifies how often this job run should be recurring') + ]); + + switch (parent::getValue('custom-frequency', RRule::DAILY)) { + case RRule::DAILY: + $this->assembleCommonElements(); + + break; + case RRule::WEEKLY: + $this->assembleCommonElements(); + $this->addElement($this->weeklyField); + + break; + case RRule::MONTHLY: + $this->assembleCommonElements(); + $this->addElement($this->monthlyFields); + + break; + case RRule::YEARLY: + $this->addElement($this->annuallyFields); + } + } elseif ($this->hasCronExpression()) { + $this->addElement('text', 'cron_expression', [ + 'required' => true, + 'label' => $this->translate('Cron Expression'), + 'description' => $this->translate('Job cron Schedule'), + 'validators' => [ + new CallbackValidator(function ($value, CallbackValidator $validator) { + if ($value && ! Cron::isValid($value)) { + $validator->addMessage($this->translate('Invalid CRON expression')); + + return false; + } + + return true; + }) + ] + ]); + } + + if ($this->getFrequency() !== static::NO_REPEAT && ! $this->hasCronExpression()) { + $this->addElement( + new Recurrence('schedule-recurrences', [ + 'id' => $this->protectId('schedule-recurrences'), + 'label' => $this->translate('Next occurrences'), + 'validate' => function (): array { + $isValid = $this->isValid(); + $reason = null; + if (! $isValid && $this->getFrequency() === static::CUSTOM_EXPR) { + if ( + $this->getCustomFrequency() !== RRule::YEARLY + && ! $this->getElement('interval')->isValid() + ) { + $reason = current($this->getElement('interval')->getMessages()); + } else { + $frequency = $this->getCustomFrequency(); + switch ($frequency) { + case RRule::WEEKLY: + $reason = current($this->weeklyField->getMessages()); + + break; + case RRule::MONTHLY: + $reason = current($this->monthlyFields->getMessages()); + + break; + default: // annually + $reason = current($this->annuallyFields->getMessages()); + + break; + } + } + } + + return [$isValid, $reason]; + }, + 'frequency' => function (): Frequency { + if ($this->getFrequency() === static::CUSTOM_EXPR) { + $rule = $this->getValue(); + } else { + $rule = RRule::fromFrequency($this->getFrequency()); + } + + $now = new DateTime(); + $start = $this->getValue('start'); + if ($start < $now) { + $now->setTime($start->format('H'), $start->format('i'), $start->format('s')); + $start = $now; + } + + $rule->startAt($start); + if ($this->getPopulatedValue('use-end-time') === 'y') { + $rule->endAt($this->getValue('end')); + } + + return $rule; + } + ]) + ); + } + } + + /** + * Assemble common parts for all the frequencies + */ + private function assembleCommonElements(): void + { + $repeat = $this->getCustomFrequency(); + if ($repeat === RRule::WEEKLY) { + $text = $this->translate('week(s) on'); + $max = 53; + } elseif ($repeat === RRule::MONTHLY) { + $text = $this->translate('month(s)'); + $max = 12; + } else { + $text = $this->translate('day(s)'); + $max = 31; + } + + $options = ['min' => 1, 'max' => $max]; + $this->addElement('number', 'interval', [ + 'class' => 'autosubmit', + 'value' => 1, + 'min' => 1, + 'max' => $max, + 'validators' => [new BetweenValidator($options)] + ]); + + $numberSpecifier = HtmlElement::create('div', ['class' => 'number-specifier']); + $element = $this->getElement('interval'); + $element->prependWrapper($numberSpecifier); + + $numberSpecifier->prependHtml(HtmlElement::create('span', null, $this->translate('Every'))); + $numberSpecifier->addHtml($element); + $numberSpecifier->addHtml(HtmlElement::create('span', null, $text)); + } + + /** + * Get prepared multipart updates + * + * @param RequestInterface $request + * + * @return array + */ + public function prepareMultipartUpdate(RequestInterface $request): array + { + $autoSubmittedBy = $request->getHeader('X-Icinga-AutoSubmittedBy'); + $pattern = '/\[(weekly-fields|monthly-fields|annually-fields)]\[(ordinal|month|day(\d+)?|[A-Z]{2})]$/'; + + $partUpdates = []; + if ( + $autoSubmittedBy + && ( + preg_match('/\[(start|end)]$/', $autoSubmittedBy[0], $matches) + || preg_match($pattern, $autoSubmittedBy[0]) + || preg_match('/\[interval]/', $autoSubmittedBy[0]) + ) + ) { + $this->ensureAssembled(); + + $partUpdates[] = $this->getElement('schedule-recurrences'); + if ( + $this->getFrequency() === static::CUSTOM_EXPR + && $this->getCustomFrequency() === RRule::MONTHLY + && isset($matches[1]) + && $matches[1] === 'start' + ) { + // To update the available fields/days based on the provided start time + $partUpdates[] = $this->monthlyFields; + } + } + + return $partUpdates; + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $attributes->registerAttributeCallback('protector', null, [$this, 'setIdProtector']); + } +} diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement/AnnuallyFields.php b/vendor/ipl/web/src/FormElement/ScheduleElement/AnnuallyFields.php new file mode 100644 index 0000000..857711a --- /dev/null +++ b/vendor/ipl/web/src/FormElement/ScheduleElement/AnnuallyFields.php @@ -0,0 +1,133 @@ +<?php + +namespace ipl\Web\FormElement\ScheduleElement; + +use InvalidArgumentException; +use ipl\Html\Attributes; +use ipl\Html\FormattedString; +use ipl\Html\FormElement\FieldsetElement; +use ipl\Html\HtmlElement; +use ipl\Web\FormElement\ScheduleElement\Common\FieldsProtector; +use ipl\Web\FormElement\ScheduleElement\Common\FieldsUtils; +use ipl\Web\Widget\Icon; + +class AnnuallyFields extends FieldsetElement +{ + use FieldsUtils; + use FieldsProtector; + + /** @var array A list of valid months */ + protected $months = []; + + /** @var string A month to preselect by default */ + protected $default = 'JAN'; + + public function __construct($name, $attributes = null) + { + $this->months = [ + 'JAN' => $this->translate('Jan'), + 'FEB' => $this->translate('Feb'), + 'MAR' => $this->translate('Mar'), + 'APR' => $this->translate('Apr'), + 'MAY' => $this->translate('May'), + 'JUN' => $this->translate('Jun'), + 'JUL' => $this->translate('Jul'), + 'AUG' => $this->translate('Aug'), + 'SEP' => $this->translate('Sep'), + 'OCT' => $this->translate('Oct'), + 'NOV' => $this->translate('Nov'), + 'DEC' => $this->translate('Dec') + ]; + + parent::__construct($name, $attributes); + } + + protected function init(): void + { + parent::init(); + $this->initUtils(); + } + + /** + * Set the default month to be activated + * + * @param string $default + * + * @return $this + */ + public function setDefault(string $default): self + { + if (! isset($this->months[strtoupper($this->default)])) { + throw new InvalidArgumentException(sprintf('Invalid month provided: %s', $default)); + } + + $this->default = strtoupper($default); + + return $this; + } + + protected function assemble() + { + $this->getAttributes()->set('id', $this->protectId('annually-fields')); + + $fieldsSelector = new FieldsRadio('month', [ + 'class' => ['autosubmit', 'sr-only'], + 'value' => $this->default, + 'options' => $this->months, + 'protector' => function ($value) { + return $this->protectId($value); + } + ]); + $this->registerElement($fieldsSelector); + + $runsOnThe = $this->getPopulatedValue('runsOnThe', 'n'); + $this->addElement('checkbox', 'runsOnThe', [ + 'class' => 'autosubmit', + 'value' => $runsOnThe + ]); + + $checkboxControls = HtmlElement::create('div', ['class' => 'toggle-slider-controls']); + $checkbox = $this->getElement('runsOnThe'); + $checkbox->prependWrapper($checkboxControls); + $checkboxControls->addHtml($checkbox, HtmlElement::create('span', null, $this->translate('On the'))); + + $annuallyWrapper = HtmlElement::create('div', ['class' => 'annually']); + $checkboxControls->prependWrapper($annuallyWrapper); + $annuallyWrapper->addHtml($fieldsSelector); + + $notes = HtmlElement::create('div', ['class' => 'note']); + $notes->addHtml( + FormattedString::create( + $this->translate('Use %s / %s keys to choose a month by keyboard.'), + new Icon('arrow-left'), + new Icon('arrow-right') + ) + ); + $annuallyWrapper->addHtml($notes); + + $enumerations = $this->createOrdinalElement(); + $enumerations->getAttributes()->set('disabled', $runsOnThe === 'n'); + $this->registerElement($enumerations); + + $selectableDays = $this->createOrdinalSelectableDays(); + $selectableDays->getAttributes()->set('disabled', $runsOnThe === 'n'); + $this->registerElement($selectableDays); + + $ordinalWrapper = HtmlElement::create('div', ['class' => ['ordinal', 'annually']]); + $this + ->decorate($enumerations) + ->addHtml($enumerations); + + $enumerations->prependWrapper($ordinalWrapper); + $ordinalWrapper->addHtml($enumerations, $selectableDays); + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $attributes + ->registerAttributeCallback('default', null, [$this, 'setDefault']) + ->registerAttributeCallback('protector', null, [$this, 'setIdProtector']); + } +} diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement/Common/FieldsProtector.php b/vendor/ipl/web/src/FormElement/ScheduleElement/Common/FieldsProtector.php new file mode 100644 index 0000000..affd519 --- /dev/null +++ b/vendor/ipl/web/src/FormElement/ScheduleElement/Common/FieldsProtector.php @@ -0,0 +1,41 @@ +<?php + +namespace ipl\Web\FormElement\ScheduleElement\Common; + +trait FieldsProtector +{ + /** @var callable */ + protected $protector; + + /** + * Set callback to protect ids with + * + * @param ?callable $protector + * + * @return $this + */ + public function setIdProtector(?callable $protector): self + { + $this->protector = $protector; + + return $this; + } + + /** + * Protect the given html id + * + * The provided id is returned as is, if no protector is specified + * + * @param string $id + * + * @return string + */ + public function protectId(string $id): string + { + if (is_callable($this->protector)) { + return call_user_func($this->protector, $id); + } + + return $id; + } +} diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement/Common/FieldsUtils.php b/vendor/ipl/web/src/FormElement/ScheduleElement/Common/FieldsUtils.php new file mode 100644 index 0000000..bf28255 --- /dev/null +++ b/vendor/ipl/web/src/FormElement/ScheduleElement/Common/FieldsUtils.php @@ -0,0 +1,243 @@ +<?php + +namespace ipl\Web\FormElement\ScheduleElement\Common; + +use DateInterval; +use DateTime; +use Exception; +use InvalidArgumentException; +use ipl\Html\Contract\FormElement; +use ipl\Scheduler\RRule; +use ipl\Web\FormElement\ScheduleElement\MonthlyFields; + +trait FieldsUtils +{ + // Non-standard frequency options + public static $everyDay = 'day'; + public static $everyWeekday = 'weekday'; + public static $everyWeekend = 'weekend'; + + // Enumerators for the monthly and annually schedule of a custom frequency + public static $first = 'first'; + public static $second = 'second'; + public static $third = 'third'; + public static $fourth = 'fourth'; + public static $fifth = 'fifth'; + public static $last = 'last'; + + private $regulars = []; + + protected function initUtils(): void + { + $this->regulars = [ + 'MO' => $this->translate('Monday'), + 'TU' => $this->translate('Tuesday'), + 'WE' => $this->translate('Wednesday'), + 'TH' => $this->translate('Thursday'), + 'FR' => $this->translate('Friday'), + 'SA' => $this->translate('Saturday'), + 'SU' => $this->translate('Sunday') + ]; + } + + protected function createOrdinalElement(): FormElement + { + return $this->createElement('select', 'ordinal', [ + 'class' => 'autosubmit', + 'value' => $this->getPopulatedValue('ordinal', static::$first), + 'options' => [ + static::$first => $this->translate('First'), + static::$second => $this->translate('Second'), + static::$third => $this->translate('Third'), + static::$fourth => $this->translate('Fourth'), + static::$fifth => $this->translate('Fifth'), + static::$last => $this->translate('Last') + ] + ]); + } + + protected function createOrdinalSelectableDays(): FormElement + { + $select = $this->createElement('select', 'day', [ + 'class' => 'autosubmit', + 'value' => $this->getPopulatedValue('day', static::$everyDay), + 'options' => $this->regulars + [ + 'separator' => '──────────────────────────', + static::$everyDay => $this->translate('Day'), + static::$everyWeekday => $this->translate('Weekday (Mon - Fri)'), + static::$everyWeekend => $this->translate('WeekEnd (Sat or Sun)') + ] + ]); + $select->getOption('separator')->getAttributes()->set('disabled', true); + + return $select; + } + + /** + * Load the given RRule instance into a list of key=>value pairs + * + * @param RRule $rule + * + * @return array + */ + public function loadRRule(RRule $rule): array + { + $values = []; + $isMonthly = $rule->getFrequency() === RRule::MONTHLY; + if ($isMonthly && (! empty($rule->getByMonthDay()) || empty($rule->getByDay()))) { + $monthDays = $rule->getByMonthDay() ?? []; + foreach (range(1, $this->availableFields) as $value) { + $values["day$value"] = in_array((string) $value, $monthDays, true) ? 'y' : 'n'; + } + + $values['runsOn'] = MonthlyFields::RUNS_EACH; + } else { + $position = $rule->getBySetPosition(); + $byDay = $rule->getByDay() ?? []; + + if ($isMonthly) { + $values['runsOn'] = MonthlyFields::RUNS_ONTHE; + } else { + $months = $rule->getByMonth(); + if (empty($months) && $rule->getStart()) { + $months[] = $rule->getStart()->format('m'); + } elseif (empty($months)) { + $months[] = date('m'); + } + + $values['month'] = strtoupper($this->getMonthByNumber((int)$months[0])); + $values['runsOnThe'] = ! empty($byDay) ? 'y' : 'n'; + } + + if (count($byDay) == 1 && preg_match('/^(-?\d)(\w.*)$/', $byDay[0], $matches)) { + $values['ordinal'] = $this->getOrdinalString($matches[1]); + $values['day'] = $this->getWeekdayName($matches[2]); + } elseif (! empty($byDay)) { + $values['ordinal'] = $this->getOrdinalString(current($position)); + switch (count($byDay)) { + case MonthlyFields::WEEK_DAYS: + $values['day'] = static::$everyDay; + + break; + case MonthlyFields::WEEK_DAYS - 2: + $values['day'] = static::$everyWeekday; + + break; + case 1: + $values['day'] = current($byDay); + + break; + case 2: + $byDay = array_flip($byDay); + if (isset($byDay['SA']) && isset($byDay['SU'])) { + $values['day'] = static::$everyWeekend; + } + } + } + } + + return $values; + } + + /** + * Transform the given expression part into a valid week day string representation + * + * @param string $day + * + * @return string + */ + public function getWeekdayName(string $day): string + { + // Not transformation is needed when the given day is part of the valid weekdays + if (isset($this->regulars[strtoupper($day)])) { + return $day; + } + + try { + // Try to figure it out using date time before raising an error + $datetime = new DateTime('Sunday'); + $datetime->add(new DateInterval("P$day" . 'D')); + + return $datetime->format('D'); + } catch (Exception $_) { + throw new InvalidArgumentException(sprintf('Invalid weekday provided: %s', $day)); + } + } + + /** + * Transform the given integer enums into something like first,second... + * + * @param string $ordinal + * + * @return string + */ + public function getOrdinalString(string $ordinal): string + { + switch ($ordinal) { + case '1': + return static::$first; + case '2': + return static::$second; + case '3': + return static::$third; + case '4': + return static::$fourth; + case '5': + return static::$fifth; + case '-1': + return static::$last; + default: + throw new InvalidArgumentException( + sprintf('Invalid ordinal string representation provided: %s', $ordinal) + ); + } + } + + /** + * Get the string representation of the given ordinal to an integer + * + * This transforms the given ordinal such as (first, second...) into its respective + * integral representation. At the moment only (1..5 + the non-standard "last") options + * are supported. So if this method returns the character "-1", is meant the last option. + * + * @param string $ordinal + * + * @return int + */ + public function getOrdinalAsInteger(string $ordinal): int + { + switch ($ordinal) { + case static::$first: + return 1; + case static::$second: + return 2; + case static::$third: + return 3; + case static::$fourth: + return 4; + case static::$fifth: + return 5; + case static::$last: + return -1; + default: + throw new InvalidArgumentException(sprintf('Invalid enumerator provided: %s', $ordinal)); + } + } + + /** + * Get a short textual representation of the given month + * + * @param int $month + * + * @return string + */ + public function getMonthByNumber(int $month): string + { + $time = DateTime::createFromFormat('!m', $month); + if ($time) { + return $time->format('M'); + } + + throw new InvalidArgumentException(sprintf('Invalid month number provided: %d', $month)); + } +} diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement/FieldsRadio.php b/vendor/ipl/web/src/FormElement/ScheduleElement/FieldsRadio.php new file mode 100644 index 0000000..31b77c3 --- /dev/null +++ b/vendor/ipl/web/src/FormElement/ScheduleElement/FieldsRadio.php @@ -0,0 +1,58 @@ +<?php + +namespace ipl\Web\FormElement\ScheduleElement; + +use ipl\Html\Attributes; +use ipl\Html\FormElement\InputElement; +use ipl\Html\FormElement\RadioElement; +use ipl\Html\HtmlElement; +use ipl\Web\FormElement\ScheduleElement\Common\FieldsProtector; + +class FieldsRadio extends RadioElement +{ + use FieldsProtector; + + protected function assemble() + { + $listItems = HtmlElement::create('ul', ['class' => ['schedule-element-fields', 'single-fields']]); + foreach ($this->options as $option) { + $radio = (new InputElement($this->getValueOfNameAttribute())) + ->setValue($option->getValue()) + ->setType($this->type); + + $radio->setAttributes(clone $this->getAttributes()); + + $htmlId = $this->protectId($option->getValue()); + $radio->getAttributes() + ->set('id', $htmlId) + ->registerAttributeCallback('checked', function () use ($option) { + return (string) $this->getValue() === (string) $option->getValue(); + }) + ->registerAttributeCallback('required', [$this, 'getRequiredAttribute']) + ->registerAttributeCallback('disabled', function () use ($option) { + return $this->getAttributes()->get('disabled')->getValue() || $option->isDisabled(); + }); + + $listItem = HtmlElement::create('li'); + $listItem->addHtml( + $radio, + HtmlElement::create('label', [ + 'for' => $htmlId, + 'class' => $option->getLabelCssClass(), + 'tabindex' => -1 + ], $option->getLabel()) + ); + $listItems->addHtml($listItem); + } + + $this->addHtml($listItems); + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $attributes + ->registerAttributeCallback('protector', null, [$this, 'setIdProtector']); + } +} diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement/MonthlyFields.php b/vendor/ipl/web/src/FormElement/ScheduleElement/MonthlyFields.php new file mode 100644 index 0000000..26329fc --- /dev/null +++ b/vendor/ipl/web/src/FormElement/ScheduleElement/MonthlyFields.php @@ -0,0 +1,191 @@ +<?php + +namespace ipl\Web\FormElement\ScheduleElement; + +use ipl\Html\Attributes; +use ipl\Html\FormElement\FieldsetElement; +use ipl\Html\HtmlElement; +use ipl\Validator\CallbackValidator; +use ipl\Validator\InArrayValidator; +use ipl\Validator\ValidatorChain; +use ipl\Web\FormElement\ScheduleElement\Common\FieldsProtector; +use ipl\Web\FormElement\ScheduleElement\Common\FieldsUtils; + +class MonthlyFields extends FieldsetElement +{ + use FieldsUtils; + use FieldsProtector; + + /** @var string Used as radio option to run each selected days/months */ + public const RUNS_EACH = 'each'; + + /** @var string Used as radio option to build complex job schedules */ + public const RUNS_ONTHE = 'onthe'; + + /** @var int Number of days in a week */ + public const WEEK_DAYS = 7; + + /** @var int Day of the month to preselect by default */ + protected $default = 1; + + /** @var int Number of fields to render */ + protected $availableFields; + + protected function init(): void + { + parent::init(); + $this->initUtils(); + + $this->availableFields = (int) date('t'); + } + + /** + * Set the available fields/days of the month to be rendered + * + * @param int $fields + * + * @return $this + */ + public function setAvailableFields(int $fields): self + { + $this->availableFields = $fields; + + return $this; + } + + /** + * Set the default field/day to be selected + * + * @param int $default + * + * @return $this + */ + public function setDefault(int $default): self + { + $this->default = $default; + + return $this; + } + + /** + * Get all the selected weekdays + * + * @return array + */ + public function getSelectedDays(): array + { + $selectedDays = []; + foreach (range(1, $this->availableFields) as $day) { + if ($this->getValue("day$day", 'n') === 'y') { + $selectedDays[] = $day; + } + } + + if (empty($selectedDays)) { + $selectedDays[] = $this->default; + } + + return $selectedDays; + } + + protected function assemble() + { + $this->getAttributes()->set('id', $this->protectId('monthly-fields')); + + $runsOn = $this->getPopulatedValue('runsOn', static::RUNS_EACH); + $this->addElement('radio', 'runsOn', [ + 'required' => true, + 'class' => 'autosubmit', + 'value' => $runsOn, + 'options' => [static::RUNS_EACH => $this->translate('Each')], + ]); + + $listItems = HtmlElement::create('ul', ['class' => ['schedule-element-fields', 'multiple-fields']]); + if ($runsOn === static::RUNS_ONTHE) { + $listItems->getAttributes()->add('class', 'disabled'); + } + + foreach (range(1, $this->availableFields) as $day) { + $checkbox = $this->createElement('checkbox', "day$day", [ + 'class' => ['autosubmit', 'sr-only'], + 'value' => $day === $this->default && $runsOn === static::RUNS_EACH + ]); + $this->registerElement($checkbox); + + $htmlId = $this->protectId("day$day"); + $checkbox->getAttributes()->set('id', $htmlId); + + $listItem = HtmlElement::create('li'); + $listItem->addHtml($checkbox, HtmlElement::create('label', ['for' => $htmlId], $day)); + $listItems->addHtml($listItem); + } + + $monthlyWrapper = HtmlElement::create('div', ['class' => 'monthly']); + $runsEach = $this->getElement('runsOn'); + $runsEach->prependWrapper($monthlyWrapper); + $monthlyWrapper->addHtml($runsEach, $listItems); + + $this->addElement('radio', 'runsOn', [ + 'required' => $runsOn !== static::RUNS_EACH, + 'class' => 'autosubmit', + 'options' => [static::RUNS_ONTHE => $this->translate('On the')], + 'validators' => [ + new InArrayValidator([ + 'strict' => true, + 'haystack' => [static::RUNS_EACH, static::RUNS_ONTHE] + ]) + ] + ]); + + $ordinalWrapper = HtmlElement::create('div', ['class' => 'ordinal']); + $runsOnThe = $this->getElement('runsOn'); + $runsOnThe->prependWrapper($ordinalWrapper); + $ordinalWrapper->addHtml($runsOnThe); + + $enumerations = $this->createOrdinalElement(); + $enumerations->getAttributes()->set('disabled', $runsOn === static::RUNS_EACH); + $this->registerElement($enumerations); + + $selectableDays = $this->createOrdinalSelectableDays(); + $selectableDays->getAttributes()->set('disabled', $runsOn === static::RUNS_EACH); + $this->registerElement($selectableDays); + + $ordinalWrapper->addHtml($enumerations, $selectableDays); + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $attributes + ->registerAttributeCallback('default', null, [$this, 'setDefault']) + ->registerAttributeCallback('availableFields', null, [$this, 'setAvailableFields']) + ->registerAttributeCallback('protector', null, [$this, 'setIdProtector']); + } + + protected function addDefaultValidators(ValidatorChain $chain): void + { + $chain->add( + new CallbackValidator(function ($_, CallbackValidator $validator): bool { + if ($this->getValue('runsOn', static::RUNS_EACH) !== static::RUNS_EACH) { + return true; + } + + $valid = false; + foreach (range(1, $this->availableFields) as $day) { + if ($this->getValue("day$day") === 'y') { + $valid = true; + + break; + } + } + + if (! $valid) { + $validator->addMessage($this->translate('You must select at least one of these days')); + } + + return $valid; + }) + ); + } +} diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement/Recurrence.php b/vendor/ipl/web/src/FormElement/ScheduleElement/Recurrence.php new file mode 100644 index 0000000..8693b20 --- /dev/null +++ b/vendor/ipl/web/src/FormElement/ScheduleElement/Recurrence.php @@ -0,0 +1,89 @@ +<?php + +namespace ipl\Web\FormElement\ScheduleElement; + +use DateTime; +use ipl\Html\Attributes; +use ipl\Html\FormElement\BaseFormElement; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\I18n\Translation; +use ipl\Scheduler\Contract\Frequency; +use ipl\Scheduler\RRule; + +class Recurrence extends BaseFormElement +{ + use Translation; + + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => 'schedule-recurrences']; + + /** @var callable A callable that generates a frequency instance */ + protected $frequencyCallback; + + /** @var callable A validation callback for the schedule element */ + protected $validateCallback; + + /** + * Set a validation callback that will be called when assembling this element + * + * @param callable $callback + * + * @return $this + */ + public function setValid(callable $callback): self + { + $this->validateCallback = $callback; + + return $this; + } + + /** + * Set a callback that generates an {@see Frequency} instance + * + * @param callable $callback + * + * @return $this + */ + public function setFrequency(callable $callback): self + { + $this->frequencyCallback = $callback; + + return $this; + } + + protected function assemble() + { + list($isValid, $reason) = ($this->validateCallback)(); + if (! $isValid) { + // Render why we can't generate the recurrences + $this->addHtml(Text::create($reason)); + + return; + } + + /** @var RRule $frequency */ + $frequency = ($this->frequencyCallback)(); + $recurrences = $frequency->getNextRecurrences(new DateTime(), 3); + if (! $recurrences->valid()) { + // Such a situation can be caused by setting an invalid end time + $this->addHtml(HtmlElement::create('p', null, Text::create($this->translate('Never')))); + + return; + } + + foreach ($recurrences as $recurrence) { + $this->addHtml(HtmlElement::create('p', null, $recurrence->format($this->translate('D, Y/m/d, H:i:s')))); + } + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $attributes + ->registerAttributeCallback('frequency', null, [$this, 'setFrequency']) + ->registerAttributeCallback('validate', null, [$this, 'setValid']); + } +} diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement/WeeklyFields.php b/vendor/ipl/web/src/FormElement/ScheduleElement/WeeklyFields.php new file mode 100644 index 0000000..01933ca --- /dev/null +++ b/vendor/ipl/web/src/FormElement/ScheduleElement/WeeklyFields.php @@ -0,0 +1,151 @@ +<?php + +namespace ipl\Web\FormElement\ScheduleElement; + +use InvalidArgumentException; +use ipl\Html\Attributes; +use ipl\Html\FormElement\FieldsetElement; +use ipl\Html\HtmlElement; +use ipl\Validator\CallbackValidator; +use ipl\Validator\ValidatorChain; +use ipl\Web\FormElement\ScheduleElement\Common\FieldsProtector; + +class WeeklyFields extends FieldsetElement +{ + use FieldsProtector; + + /** @var array A list of valid week days */ + protected $weekdays = []; + + /** @var string A valid weekday to be selected by default */ + protected $default = 'MO'; + + public function __construct($name, $attributes = null) + { + $this->weekdays = [ + 'MO' => $this->translate('Mon'), + 'TU' => $this->translate('Tue'), + 'WE' => $this->translate('Wed'), + 'TH' => $this->translate('Thu'), + 'FR' => $this->translate('Fri'), + 'SA' => $this->translate('Sat'), + 'SU' => $this->translate('Sun') + ]; + + parent::__construct($name, $attributes); + } + + /** + * Set the default weekday to be preselected + * + * @param string $default + * + * @return $this + */ + public function setDefault(string $default): self + { + $weekday = strlen($default) > 2 ? substr($default, 0, -1) : $default; + if (! isset($this->weekdays[strtoupper($weekday)])) { + throw new InvalidArgumentException(sprintf('Invalid weekday provided: %s', $default)); + } + + $this->default = strtoupper($weekday); + + return $this; + } + + /** + * Get all the selected weekdays + * + * @return array + */ + public function getSelectedWeekDays(): array + { + $selectedDays = []; + foreach ($this->weekdays as $day => $_) { + if ($this->getValue($day, 'n') === 'y') { + $selectedDays[] = $day; + } + } + + if (empty($selectedDays)) { + $selectedDays[] = $this->default; + } + + return $selectedDays; + } + + /** + * Transform the given weekdays into key=>value array that can be populated + * + * @param array $weekdays + * + * @return array + */ + public function loadWeekDays(array $weekdays): array + { + $values = []; + foreach ($this->weekdays as $weekday => $_) { + $values[$weekday] = in_array($weekday, $weekdays, true) ? 'y' : 'n'; + } + + return $values; + } + + protected function assemble() + { + $this->getAttributes()->set('id', $this->protectId('weekly-fields')); + + $fieldsWrapper = HtmlElement::create('div', ['class' => 'weekly']); + $listItems = HtmlElement::create('ul', ['class' => ['schedule-element-fields', 'multiple-fields']]); + + foreach ($this->weekdays as $day => $value) { + $checkbox = $this->createElement('checkbox', $day, [ + 'class' => ['autosubmit', 'sr-only'], + 'value' => $day === $this->default + ]); + $this->registerElement($checkbox); + + $htmlId = $this->protectId("weekday-$day"); + $checkbox->getAttributes()->set('id', $htmlId); + + $listItem = HtmlElement::create('li'); + $listItem->addHtml($checkbox, HtmlElement::create('label', ['for' => $htmlId], $value)); + $listItems->addHtml($listItem); + } + + $fieldsWrapper->addHtml($listItems); + $this->addHtml($fieldsWrapper); + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $attributes + ->registerAttributeCallback('default', null, [$this, 'setDefault']) + ->registerAttributeCallback('protector', null, [$this, 'setIdProtector']); + } + + protected function addDefaultValidators(ValidatorChain $chain): void + { + $chain->add( + new CallbackValidator(function ($_, CallbackValidator $validator): bool { + $valid = false; + foreach ($this->weekdays as $weekday => $_) { + if ($this->getValue($weekday) === 'y') { + $valid = true; + + break; + } + } + + if (! $valid) { + $validator->addMessage($this->translate('You must select at least one of these weekdays')); + } + + return $valid; + }) + ); + } +} |