Files
cacti/lib/api_scheduler.php
TheWitnessandTheWitness 3ab4c69fb7 PHPStan Level 8 - Phase 1 Conversion (#8135)
* PHPStan Level 8 - Phase 1 Conversion

Reduces the real (no-baseline) PHPStan level 8 error count from ~2,724 to 425
and regenerates phpstan-baseline.neon accordingly (8,941 -> 1,921 lines).

Core type-safety changes
- lib/database.php: narrow db_fetch_row()/db_fetch_row_prepared() to return
  array (was bool|array) and db_fetch_assoc()/db_fetch_assoc_prepared() to
  return array (was mixed); the _prepared workers coerce the connection-failure
  path to an empty array so the declared type is honest. empty()/cacti_sizeof()
  checks are unaffected.
- lib/database.php: narrow $db_conn guards from is_object() to instanceof PDO in
  the transaction/quote/insert-id helpers so the PDO calls resolve.
- lib/mib_cache.php: the fluent MibCache methods mib()/object()/table()/row()
  now return self instead of a bare object so chained calls type-check.

Type-safety casts and guards (mechanical, behaviour-preserving)
- Cast string|false/|null builtin results feeding string parameters
  (json_decode, base64_encode, trim, htmle, preg_replace, explode, etc.).
- Cast int|string / int|false values feeding int id/pid parameters.
- Guard resource|false fopen()/popen() handles before fwrite/fclose/fread, and
  wrap foreach over array|false/list|false results with `?: []`.

Genuine bugs found and fixed
- cli/add_tree.php / lib/api_tree.php: site tree nodes could not be created via
  the CLI - the --site-id option was never parsed, $nodeTypes had no 'site'
  entry (passing null to api_tree_item_save(int $type) -> TypeError), and
  api_tree_item_save() never persisted site_id. All three fixed.
- tree.php: leaves_exist() is typed : int but returned db_fetch_assoc_prepared()
  (an array of rows) for a SELECT COUNT(*); now uses db_fetch_cell_prepared()
  and returns the scalar count.
- lib/mib_cache.php: $entries_per_object was a float (division / ceil) used as
  an array key (PHP 8.1 implicit float-to-int key deprecation); now cast to int.

Config
- .phpstan.neon: add notIdentical.alwaysTrue and booleanAnd.alwaysFalse to the
  existing always-true/false ignore family (same "PHPStan gets confused"
  dynamic-constant cause already covered by identical.alwaysTrue,
  notEqual.alwaysTrue, booleanAnd.leftAlwaysTrue, etc.).
- phpstan-baseline.neon: regenerated; the remaining 425 entries are the
  untouched long tail for Phase 2.

* Remove redundant @phpstan-ignore comments

Follow-up cleanup for the level 8 Phase 1 work. Removes 14 inline
@phpstan-ignore comments that no longer suppress any reported error:

- cli/repair_database.php: `@phpstan-ignore booleanAnd.alwaysFalse` is now
  covered by the global booleanAnd.alwaysFalse ignore added to .phpstan.neon.
- 13 others (host.php, include/global.php, include/session.php,
  lib/api_automation.php x2, lib/functions.php x2, lib/html_utility.php x2,
  lib/installer.php, lib/package.php, lib/reports.php, lib/rrd.php) no longer
  match any reported error after the type-safety fixes (e.g. the
  db_fetch_row_prepared() return-type narrowing removed the offset-assign
  errors the api_automation ignores were suppressing).

Verified with reportUnmatchedIgnoredErrors enabled: 0 unmatched ignores
remain and the full level 8 run still reports 0 errors.

* ci: satisfy validator ratchet and refresh security sink baselines

- lib/api_tree.php: give the new tree-item site_id a real numeric
  constraint ('^[-0-9]+$', matching html_reports.php) instead of an
  empty regex, so the validator constraint ratchet passes without
  bumping its baseline.
- tests/security/baselines: refresh the three sink/hotspot signatures
  whose code text changed from the (string) casts added in this branch
  (lib/poller.php setsid, cli/rrdresize.php json_encode, lib/import.php
  simplexml_load_string).

* ci(js): allow DOMPurify 3.4.16 and update sync_js pin test

The dependabot bump (#8134) moved package.json/package-lock.json to
DOMPurify 3.4.16 on develop but left tests/js/sync_js.test.mjs pinned to
3.4.15, so the PR merge commit fails the 'DOMPurify is pinned' contract.
Bring package.json/package-lock.json to 3.4.16 (matching develop) and
update the pin test to expect 3.4.16.

* test: update sql.php null-handling assertion for the (string) cast

This branch changed scripts/sql.php from trim(\ ?? '') to
trim((string) \) (the phpstan-safe form that coerces both null and
false from shell_exec()). Update SqlScriptsTest's null-return assertion
to match the new cast instead of the removed '?? ''.

* fix: address Copilot review - ownership IDs, fopen guards, mib_cache bound

- chown()/chgrp(): pass integer UID/GID from fileowner()/filegroup()
  (guarding the false lookup) instead of string-casting them, which made
  them resolve account/group names like '1001' and fail. Covers
  lib/api_device.php, lib/boost.php and poller_maintenance.php (x2).
- fopen() failure now short-circuits instead of reporting success:
  lib/package.php returns false, lib/clog_webapi.php and utilities.php
  render an error, and snmpagent_mibcachechild.php skips the cache write
  and sentinel unlink so a concurrent writer's lock is preserved.
- lib/mib_cache.php: keep the float quotient as the loop bound and cast
  only the array offset, so a non-even entries/columns split no longer
  truncates the loop and drops a cached row.

* revert: keep db_fetch_* returning false on error; baseline Level 8 instead

Per review, restoring the intentional fail-safe: db_fetch_row[_prepared]()
return bool|array and db_fetch_assoc[_prepared]() return mixed again, so a
genuine SQL error (not just a missing connection) is signalled as false and
the === false checks in cdef_is_in_use(), boost_process_* and aggregate.php
work as before. Dropped the is_array() ? : [] coercion.

Level 8 stays green by regenerating phpstan-baseline.neon to capture the
re-introduced bool|array/mixed call-site errors (the ~1,850 the narrowing
had eliminated). Tightening those callers to drop them from the baseline is
Phase 2 work.

---------

Co-authored-by: TheWitness <copilot@users.noreply.github.com>
2026-10-02 10:08:56 -04:00

806 lines
22 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
/**
* Generates the form configuration array for the API scheduler.
*
* @return array The form configuration array.
*/
function api_scheduler_form() : array {
global $sched_types, $heartbeats;
return [
'spacer2' => [
'method' => 'spacer',
'friendly_name' => __('Schedule'),
'collapsible' => 'true'
],
'sched_type' => [
'method' => 'drop_array',
'friendly_name' => __('Schedule Type'),
'description' => __('Define the collection frequency.'),
'value' => '|arg1:sched_type|',
'array' => $sched_types,
'default' => SCHEDULE_MANUAL
],
'start_at' => [
'method' => 'textbox',
'friendly_name' => __('Starting Date/Time'),
'description' => __('What time will this Network discover item start?'),
'value' => '|arg1:start_at|',
'max_length' => '30',
'default' => date('Y-m-d H:i:s'),
'size' => 60
],
'recur_every' => [
'method' => 'drop_array',
'friendly_name' => __('Rerun Every'),
'description' => __('Rerun discovery for this Network Range every X.'),
'value' => '|arg1:recur_every|',
'default' => '1',
'array' => [
1 => '1',
2 => '2',
3 => '3',
4 => '4',
5 => '5',
6 => '6',
7 => '7'
],
],
'day_of_week' => [
'method' => 'drop_multi',
'friendly_name' => __('Days of Week'),
'description' => __('What Day(s) of the week will this Network Range be discovered.'),
'array' => [
1 => __('Sunday'),
2 => __('Monday'),
3 => __('Tuesday'),
4 => __('Wednesday'),
5 => __('Thursday'),
6 => __('Friday'),
7 => __('Saturday')
],
'value' => '|arg1:day_of_week|',
'class' => 'multiselect day_of_week'
],
'month' => [
'method' => 'drop_multi',
'friendly_name' => __('Months of Year'),
'description' => __('What Months(s) of the Year will this Network Range be discovered.'),
'array' => [
1 => __('January'),
2 => __('February'),
3 => __('March'),
4 => __('April'),
5 => __('May'),
6 => __('June'),
7 => __('July'),
8 => __('August'),
9 => __('September'),
10 => __('October'),
11 => __('November'),
12 => __('December')
],
'value' => '|arg1:month|',
'class' => 'multiselect month'
],
'day_of_month' => [
'method' => 'drop_multi',
'friendly_name' => __('Days of Month'),
'description' => __('What Day(s) of the Month will this Network Range be discovered.'),
'array' => [1 => '1', 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 => __('Last')],
'value' => '|arg1:day_of_month|',
'class' => 'multiselect days_of_month'
],
'monthly_week' => [
'method' => 'drop_multi',
'friendly_name' => __('Week(s) of Month'),
'description' => __('What Week(s) of the Month will this Network Range be discovered.'),
'array' => [
1 => __('First'),
2 => __('Second'),
3 => __('Third'),
'32' => __('Last')
],
'value' => '|arg1:monthly_week|',
'class' => 'multiselect monthly_week'
],
'monthly_day' => [
'method' => 'drop_multi',
'friendly_name' => __('Day(s) of Week'),
'description' => __('What Day(s) of the week will this Network Range be discovered.'),
'array' => [
1 => __('Sunday'),
2 => __('Monday'),
3 => __('Tuesday'),
4 => __('Wednesday'),
5 => __('Thursday'),
6 => __('Friday'),
7 => __('Saturday')
],
'value' => '|arg1:monthly_day|',
'class' => 'multiselect monthly_day'
],
'run_limit' => [
'method' => 'drop_array',
'friendly_name' => __('Run Limit'),
'description' => __('Define the maximum allowed runtime of this scheduled task.'),
'value' => '|arg1:run_limit|',
'array' => $heartbeats,
'default' => '20'
],
'next_start' => [
'method' => 'hidden',
'value' => '|arg1:next_start|'
],
'orig_sched_type' => [
'method' => 'hidden',
'value' => '|arg1:sched_type|'
],
'orig_start_at' => [
'method' => 'hidden',
'value' => '|arg1:start_at|'
],
];
}
/**
* Outputs the JavaScript code for the scheduler functionality.
*
* @return void
*/
function api_scheduler_javascript() : void {
?>
<script type='text/javascript'>
$(function() {
$('#day_of_week').multiselect({
selectedList: 7,
noneSelectedText: '<?php print __('Select the days(s) of the week'); ?>',
header: false,
height: 54,
groupColumns: true,
groupColumnsWidth: 90,
menuWidth: 385
});
$('#month').multiselect({
selectedList: 7,
noneSelectedText: '<?php print __('Select the month(s) of the year'); ?>',
header: false,
height: 82,
groupColumns: true,
groupColumnsWidth: 90,
menuWidth: 380
});
$('#day_of_month').multiselect({
selectedList: 15,
noneSelectedText: '<?php print __('Select the day(s) of the month'); ?>',
header: false,
height: 162,
groupColumns: true,
groupColumnsWidth: 50,
menuWidth: 275
});
$('#monthly_week').multiselect({
selectedList: 4,
noneSelectedText: '<?php print __('Select the week(s) of the month'); ?>',
header: false,
height: 28,
groupColumns: true,
groupColumnsWidth: 70,
menuWidth: 300
});
$('#monthly_day').multiselect({
selectedList: 7,
noneSelectedText: '<?php print __('Select the day(s) of the week'); ?>',
header: false,
height: 54,
groupColumns: true,
groupColumnsWidth: 90,
menuWidth: 385
});
$('#start_at').datetimepicker({
minuteGrid: 10,
stepMinute: 5,
timeFormat: 'HH:mm',
dateFormat: 'yy-mm-dd',
minDateTime: new Date(<?php print date('Y') . ', ' . (date('m') - 1) . ', ' . date('d, H') . ', ' . date('i', intval(ceil(time() / 300)) * 300) . ', 0, 0'; ?>)
});
$('#sched_type').change(function() {
setSchedule();
});
setSchedule();
});
function setSchedule() {
var schedType = $('#sched_type').val();
toggleFields({
start_at: schedType > 1,
recur_every: (schedType > 1 && schedType < 4) || schedType == 6,
day_of_week: schedType == 3,
month: schedType > 3 && schedType != 6,
day_of_month: schedType == 4,
monthly_week: schedType == 5,
monthly_day: schedType == 5,
});
if (schedType == 2) { // Daily
$('#row_recur_every').find('div:first').each(function() {
var html = $(this).html();
if (html.indexOf('X Weeks') >= 0) {
html = html.replace('<?php print __('every X Weeks'); ?>', '<?php print __('every X Days'); ?>');
html = html.replace('<?php print __('Rerun Every X Weeks'); ?>', '<?php print __('Rerun Every X Days'); ?>');
} else if (html.indexOf('X Hours') >= 0) {
html = html.replace('<?php print __('Rerun Every X Hours'); ?>', '<?php print __('Rerun Every X Days'); ?>');
html = html.replace('<?php print __('every X Hours'); ?>', '<?php print __('every X Days'); ?>');
} else if (html.indexOf('X Days') < 0) {
html = html.replace('<?php print __('Rerun Every'); ?>', '<?php print __('Rerun Every X Days'); ?>');
html = html.replace('<?php print __('every X'); ?>', '<?php print __('every X Days'); ?>');
}
$(this).html(html);
});
} else if (schedType == 6) { // Hourly
$('#row_recur_every').find('div:first').each(function() {
var html = $(this).html();
if (html.indexOf('X Weeks') >= 0) {
html = html.replace('<?php print __('every X Weeks'); ?>', '<?php print __('every X Hours'); ?>');
html = html.replace('<?php print __('Rerun Every X Weeks'); ?>', '<?php print __('Rerun Every X Hours'); ?>');
} else if (html.indexOf('X Days') >= 0) {
html = html.replace('<?php print __('Rerun Every X Days'); ?>', '<?php print __('Rerun Every X Hours'); ?>');
html = html.replace('<?php print __('every X Days'); ?>', '<?php print __('every X Hours'); ?>');
} else if (html.indexOf('X Hours') < 0) {
html = html.replace('<?php print __('Rerun Every'); ?>', '<?php print __('Rerun Every X Hours'); ?>');
html = html.replace('<?php print __('every X'); ?>', '<?php print __('every X Hours'); ?>');
}
$(this).html(html);
});
} else if (schedType == 3) { //Weekly
$('#row_recur_every').find('div:first').each(function() {
var html = $(this).html();
if (html.indexOf('X Days') >= 0) {
html = html.replace('<?php print __('every X Days'); ?>', '<?php print __('every X Weeks'); ?>');
html = html.replace('<?php print __('Rerun Every X Days'); ?>', '<?php print __('Rerun Every X Weeks'); ?>');
} else if (html.indexOf('X Hours') >= 0) {
html = html.replace('<?php print __('Rerun Every X Hours'); ?>', '<?php print __('Rerun Every X Weeks'); ?>');
html = html.replace('<?php print __('every X Hours'); ?>', '<?php print __('every X Weeks'); ?>');
} else if (html.indexOf('X Weeks') < 0) {
html = html.replace('<?php print __('Rerun Every'); ?>', '<?php print __('Rerun Every X Weeks'); ?>');
html = html.replace('<?php print __('every X'); ?>', '<?php print __('every X Weeks'); ?>');
}
$(this).html(html);
});
}
}
</script>
<?php
}
/**
* Augments the save array with scheduler settings and validates the input data.
*
* @param array $save The array to be augmented with scheduler settings.
* @param array $post The array containing the input data to be validated and saved.
*
* @return array The augmented save array with validated scheduler settings.
*/
function api_scheduler_augment_save(array $save, array $post) : array {
// scheduler settings
$save['sched_type'] = form_input_validate($post['sched_type'], 'sched_type', '^[0-9]+$', false, 3);
$save['start_at'] = form_input_validate($post['start_at'], 'start_at', '', false, 3);
// accommodate a schedule start change
if (isset($post['orig_start_at']) && $post['orig_start_at'] != $post['start_at']) {
$save['next_start'] = '0000-00-00';
}
if (isset($post['orig_sched_type']) && $post['orig_sched_type'] != $post['sched_type']) {
$save['next_start'] = '0000-00-00';
}
$save['recur_every'] = form_input_validate($post['recur_every'], 'recur_every', '', true, 3);
$save['run_limit'] = form_input_validate($post['run_limit'], 'run_limit', '', true, 3);
// convert arrays to strings
$variables = ['day_of_week', 'month', 'day_of_month', 'monthly_week', 'monthly_day'];
$aposts = [];
foreach ($variables as $v) {
if (isset($post[$v])) {
$save[$v] = form_input_validate(implode(',', $post[$v]), $v, '', true, 3);
} else {
$save[$v] = '';
}
}
// check for bad rules
if ($save['sched_type'] == SCHEDULE_WEEKLY) {
if ($save['day_of_week'] == '') {
$save['enabled'] = '';
raise_message('sched_err', __esc('ERROR: You must specify the day of the week. Disabling Network %s!.', $save['name']), MESSAGE_LEVEL_ERROR);
}
} elseif ($save['sched_type'] == SCHEDULE_MONTHLY) {
if ($save['month'] == '' || $save['day_of_month'] == '') {
$save['enabled'] = '';
raise_message('sched_err', __esc('ERROR: You must specify both the Months and Days of Month. Disabling Network %s!', $save['name']), MESSAGE_LEVEL_ERROR);
}
} elseif ($save['sched_type'] == SCHEDULE_MONTHLY_ON_DAY) {
if ($save['month'] == '' || $save['monthly_day'] == '' || $save['monthly_week'] == '') {
$save['enabled'] = '';
raise_message('sched_err', __esc('ERROR: You must specify the Months, Weeks of Months, and Days of Week. Disabling Network %s!', $save['name']), MESSAGE_LEVEL_ERROR);
}
}
$now_time = time();
if (isset($save['next_start'])) {
$next_start = strtotime($save['next_start']);
} else {
$next_start = 0;
}
$start_at = strtotime($save['start_at']);
$poller_int = read_config_option('poller_interval');
/**
* The next_start is really when the report will be checked if it's time to
* start not the time the report will actually be run. So, the numbers can
* be a little loose up front.
*
* The schedulers check will actually adjust the actual next start
* when it performs the first check.
*/
if ($save['sched_type'] != 1) {
if ($next_start === '0000-00-00 00:00:00') {
$save['next_start'] = date('Y-m-d H:i:s', $start_at);
}
if ($start_at + $poller_int < $now_time) {
// adjust to todays date and check if it's in the past
$timestamp = strtotime('12:00am') + date('H', $start_at) * 3600 + date('i', $start_at) * 60 + date('s', $start_at);
if ($timestamp < $now_time + $poller_int) {
// if the time is in the past, adjust forward by one day
$timestamp += 86400;
}
$save['next_start'] = date('Y-m-d H:i:s', (int) $timestamp);
} else {
// the time is in the future, we are safe to store it
$save['next_start'] = date('Y-m-d H:i:s', $start_at);
}
}
return $save;
}
/**
* Determines if it is time to start a scheduled task based on the provided schedule.
*
* @param array $schedule The schedule details
* @param string $table The name of the table to update the next start time in. Default is 'automation_networks'.
*
* @return bool Returns true if it is time to start the scheduled task, false otherwise.
*/
function api_scheduler_is_time_to_start(array $schedule, string $table = 'automation_networks') : bool {
$now = time();
if (empty($schedule['next_start'])) {
$schedule['next_start'] = date('Y-m-d H:i:s', strtotime($schedule['start_at']) + 86400);
}
if (is_null($schedule['start_at'])) {
$schedule['start_at'] = date('Y-m-d H:i:s');
}
switch($schedule['sched_type']) {
case SCHEDULE_MANUAL:
return false;
case SCHEDULE_HOURLY:
case SCHEDULE_DAILY:
if ($schedule['sched_type'] == SCHEDULE_HOURLY) {
$recur = $schedule['recur_every'] * 3600; // days
} else {
$recur = $schedule['recur_every'] * 86400; // days
}
$start = strtotime($schedule['start_at']);
$next = strtotime($schedule['next_start']);
if ($schedule['next_start'] == '0000-00-00 00:00:00') {
$target = $start;
} else {
$target = $next;
}
if ($now > $target) {
while ($now > $target) {
$target += $recur;
}
db_execute_prepared("UPDATE $table
SET next_start = ?
WHERE id = ?",
[date('Y-m-d H:i', $target), $schedule['id']]);
return true;
}
return false;
case SCHEDULE_WEEKLY:
$recur = $schedule['recur_every'] * 86400 * 7; // weeks
$start = strtotime($schedule['start_at']);
$next = strtotime($schedule['next_start']);
$days = explode(',', $schedule['day_of_week']);
$day = 86400;
$week = 86400 * 7;
if ($schedule['next_start'] == '0000-00-00 00:00:00') {
$target = $start;
} else {
$target = $next;
}
if ($now > $target) {
while (true) {
$target += $day;
$cur_day = date('w', $target) + 1;
$key = array_search($cur_day, $days, false);
if ($key !== false) {
if ($key == 0) {
$target += $recur - $week;
}
break;
}
}
db_execute_prepared("UPDATE $table
SET next_start = ?
WHERE id = ?",
[date('Y-m-d H:i', $target), $schedule['id']]);
return true;
}
return false;
case SCHEDULE_MONTHLY:
case SCHEDULE_MONTHLY_ON_DAY:
$next = api_scheduler_calculate_next_start($schedule);
/* An unschedulable configuration returns false, and date() would
turn that into 1970, which the run check below then reads as
overdue on every poller cycle. Leave next_start alone. The test
is strict: a calculated timestamp of 0 is a different thing from
"nothing to schedule", and should not be silently equated. */
if ($next === false) {
return false;
}
db_execute_prepared("UPDATE $table
SET next_start = ?
WHERE id = ?",
[date('Y-m-d H:i', $next), $schedule['id']]);
if ($schedule['next_start'] == '0000-00-00 00:00:00') {
if ($now > strtotime($schedule['start_at'])) {
return true;
} else {
return false;
}
} elseif ($now > strtotime($schedule['next_start'])) {
return true;
}
return false;
}
return false;
}
/**
* Calculate the next start time for a given schedule.
*
* @param array $schedule The schedule configuration array.
*
* @return mixed - The timestamp of the next start time, or false if no valid next start time is found.
*/
function api_scheduler_calculate_next_start(array $schedule) : mixed {
$now = time();
/* Try this year, then next. Without the rollover every selected date is in
the past by December and the schedule resolves to false for the rest of
the year. */
$next = api_scheduler_calculate_next_start_for_year($schedule, (int) date('Y', $now), $now);
if ($next === false) {
$next = api_scheduler_calculate_next_start_for_year($schedule, (int) date('Y', $now) + 1, $now);
}
return $next;
}
/**
* Calculate the next start time for a given schedule within one calendar year.
*
* @param array $schedule The schedule configuration array.
* @param int $year The calendar year to resolve the schedule against.
* @param int $now The timestamp a candidate date has to be later than.
*
* @return mixed - The timestamp of the next start time, or false if no valid next start time is found.
*/
function api_scheduler_calculate_next_start_for_year(array $schedule, int $year, int $now) : mixed {
$dates = [];
// Some defaults
$smonth = 'January';
$sweek = 'first';
$sday = 'Sunday';
switch($schedule['sched_type']) {
case SCHEDULE_MANUAL:
break;
case SCHEDULE_MONTHLY:
$months = explode(',', $schedule['month']);
$days = explode(',', $schedule['day_of_month']);
foreach ($months as $month) {
foreach ($days as $day) {
switch($month) {
case '1':
$smonth = 'January';
break;
case '2':
$smonth = 'February';
break;
case '3':
$smonth = 'March';
break;
case '4':
$smonth = 'April';
break;
case '5':
$smonth = 'May';
break;
case '6':
$smonth = 'June';
break;
case '7':
$smonth = 'July';
break;
case '8':
$smonth = 'August';
break;
case '9':
$smonth = 'September';
break;
case '10':
$smonth = 'October';
break;
case '11':
$smonth = 'November';
break;
case '12':
$smonth = 'December';
break;
}
if ($day == '32') {
$dates[] = strtotime("last day of $smonth $year");
} else {
$dates[] = strtotime("$smonth $day $year");
}
}
}
break;
case SCHEDULE_MONTHLY_ON_DAY:
$months = explode(',', $schedule['month']);
$weeks = explode(',', $schedule['monthly_week']);
$days = explode(',', $schedule['monthly_day']);
foreach ($months as $month) {
foreach ($weeks as $week) {
foreach ($days as $day) {
switch($month) {
case '1':
$smonth = 'January';
break;
case '2':
$smonth = 'February';
break;
case '3':
$smonth = 'March';
break;
case '4':
$smonth = 'April';
break;
case '5':
$smonth = 'May';
break;
case '6':
$smonth = 'June';
break;
case '7':
$smonth = 'July';
break;
case '8':
$smonth = 'August';
break;
case '9':
$smonth = 'September';
break;
case '10':
$smonth = 'October';
break;
case '11':
$smonth = 'November';
break;
case '12':
$smonth = 'December';
break;
default:
$smonth = 'January';
break;
}
switch($week) {
case '1':
$sweek = 'first';
break;
case '2':
$sweek = 'second';
break;
case '3':
$sweek = 'third';
break;
case '4':
$sweek = 'fourth';
break;
case '32':
$sweek = 'last';
break;
default:
$sweek = 'first';
break;
}
switch($day) {
case '1':
$sday = 'Sunday';
break;
case '2':
$sday = 'Monday';
break;
case '3':
$sday = 'Tuesday';
break;
case '4':
$sday = 'Wednesday';
break;
case '5':
$sday = 'Thursday';
break;
case '6':
$sday = 'Friday';
break;
case '7':
$sday = 'Saturday';
break;
default:
$sday = 'Sunday';
break;
}
$dates[] = strtotime("$sweek $sday of $smonth $year");
}
}
}
break;
}
if ($schedule['sched_type'] !== SCHEDULE_MANUAL) {
asort($dates);
$newdates = [];
foreach ($dates as $date) {
// an unparsable month/week/day combination yields false, not a date
if ($date === false) {
continue;
}
$ndate = date('Y-m-d', $date) . ' ' . date('H:i:s', strtotime($schedule['start_at']));
$ntime = strtotime($ndate);
cacti_log('Start At: ' . $schedule['start_at'] . ', Possible Next Start: ' . $ndate . ' with Timestamp: ' . $ntime, false, 'SCHEDULER', POLLER_VERBOSITY_DEBUG);
if ($ntime > $now) {
return $ntime;
}
}
}
return false;
}