diff --git a/application/config/migration.php b/application/config/migration.php
index aebf9841dc..ce5a1727a3 100644
--- a/application/config/migration.php
+++ b/application/config/migration.php
@@ -22,7 +22,7 @@
|
*/
-$config['migration_version'] = 195;
+$config['migration_version'] = 196;
/*
|--------------------------------------------------------------------------
diff --git a/application/controllers/Clublog.php b/application/controllers/Clublog.php
index 83815ac012..ad61ec00a8 100644
--- a/application/controllers/Clublog.php
+++ b/application/controllers/Clublog.php
@@ -25,6 +25,10 @@ public function index() {
public function upload() {
$this->load->model('clublog_model');
+ // set the last run in cron table for the correct cron id
+ $this->load->model('cron_model');
+ $this->cron_model->set_last_run($this->router->class.'_'.$this->router->method);
+
$users = $this->clublog_model->get_clublog_users();
foreach ($users as $user) {
diff --git a/application/controllers/Cron.php b/application/controllers/Cron.php
new file mode 100644
index 0000000000..af0c80d669
--- /dev/null
+++ b/application/controllers/Cron.php
@@ -0,0 +1,262 @@
+session->userdata('user_id') == '') {
+ echo "Maintenance Mode is active. Try again later.\n";
+ redirect('user/login');
+ }
+
+ $this->load->model('cron_model');
+ }
+
+ public function index() {
+
+ $this->load->model('user_model');
+ if (!$this->user_model->authorize(99)) {
+ $this->session->set_flashdata('notice', 'You\'re not allowed to do that!');
+ redirect('dashboard');
+ }
+
+ $this->load->helper('file');
+
+ $footerData = [];
+ $footerData['scripts'] = [
+ 'assets/js/cronstrue.min.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/cronstrue.min.js")),
+ 'assets/js/sections/cron.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/sections/cron.js"))
+ ];
+
+ $data['page_title'] = "Cron Manager";
+ $data['crons'] = $this->cron_model->get_crons();
+
+ $mastercron = array();
+ $mastercron = $this->get_mastercron_status();
+ $data['mastercron'] = $mastercron;
+
+ $this->load->view('interface_assets/header', $data);
+ $this->load->view('cron/index');
+ $this->load->view('interface_assets/footer', $footerData);
+ }
+
+ public function run() {
+
+ // This is the main function, which handles all crons, runs them if enabled and writes the 'next run' timestamp to the database
+
+ // TODO Add an API Key to the cronjob to improve security?
+
+ $crons = $this->cron_model->get_crons();
+
+ $status = 'pending';
+
+ foreach ($crons as $cron) {
+ if ($cron->enabled == 1) {
+
+ // calculate the crons expression
+ $data = array(
+ 'expression' => $cron->expression,
+ 'timeZone' => null
+ );
+ $this->load->library('CronExpression', $data);
+
+ $cronjob = $this->cronexpression;
+ $dt = new DateTime();
+ $isdue = $cronjob->isMatching($dt);
+
+ $next_run = $cronjob->getNext();
+ $next_run_date = date('Y-m-d H:i:s', $next_run);
+ $this->cron_model->set_next_run($cron->id, $next_run_date);
+
+ if ($isdue == true) {
+ $isdue_result = 'true';
+
+ // TODO Add log_message level debug here to have logging for the cron manager
+
+ echo "CRON: " . $cron->id . " -> is due: " . $isdue_result . "\n";
+ echo "CRON: " . $cron->id . " -> RUNNING...\n";
+
+ $url = base_url() . $cron->function;
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_HEADER, false);
+ curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog Updater');
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ $crun = curl_exec($ch);
+ curl_close($ch);
+
+ if ($crun !== false) {
+ echo "CRON: " . $cron->id . " -> CURL Result: " . $crun . "\n";
+ $status = 'healthy';
+ } else {
+ echo "ERROR: Something went wrong with " . $cron->id . "\n";
+ $status = 'failed';
+ }
+ } else {
+ $isdue_result = 'false';
+ echo "CRON: " . $cron->id . " -> is due: " . $isdue_result . " -> Next Run: " . $next_run_date . "\n";
+ $status = 'healthy';
+ }
+ } else {
+ echo 'CRON: ' . $cron->id . " is disabled. skipped..\n";
+ $status = 'disabled';
+
+ // Set the next_run timestamp to null to indicate in the view/database that this cron is disabled
+ $this->cron_model->set_next_run($cron->id, null);
+ }
+ $this->cron_model->set_status($cron->id, $status);
+ $this->cronexpression = null;
+ }
+
+ $datetime = new DateTime("now", new DateTimeZone('UTC'));
+ $datetime = $datetime->format('Ymd H:i:s');
+ $this->optionslib->update('mastercron_last_run', $datetime , 'no');
+ }
+
+ public function editDialog() {
+
+ $cron_query = $this->cron_model->cron(xss_clean($this->input->post('id', true)));
+
+ $data['cron'] = $cron_query->row();
+ $data['page_title'] = "Edit Cronjob";
+
+ $this->load->view('cron/edit', $data);
+ }
+
+ public function edit() {
+ $this->load->model('user_model');
+ if (!$this->user_model->authorize(99)) {
+ $this->session->set_flashdata('notice', 'You\'re not allowed to do that!');
+ redirect('dashboard');
+ }
+
+ $id = xss_clean($this->input->post('cron_id', true));
+ $description = xss_clean($this->input->post('cron_description', true));
+ $expression = xss_clean($this->input->post('cron_expression', true));
+ $enabled = xss_clean($this->input->post('cron_enabled', true));
+
+ $data = array(
+ 'expression' => $expression,
+ 'timeZone' => null
+ );
+ $this->load->library('CronExpression', $data);
+ $cron = $this->cronexpression;
+
+ if ($cron->isValid()) {
+ $this->cron_model->edit_cron($id, $description, $expression, $enabled);
+ $this->cronexpression = null;
+
+ header("Content-type: application/json");
+ echo json_encode(['success' => true, 'messagecategory' => 'success', 'message' => 'Changes saved for Cronjob "' . $id . '"']);
+ } else {
+ $this->session->set_flashdata('error', 'The Cron Expression you entered is not valid');
+ $this->cronexpression = null;
+
+ header("Content-type: application/json");
+ echo json_encode(['success' => false, 'messagecategory' => 'error', 'message' => 'The expression "' . $expression . '" is not valid. Please try again.']);
+ }
+ }
+
+ public function toogleEnableCronSwitch() {
+
+ $id = xss_clean($this->input->post('id', true));
+ $cron_enabled = xss_clean($this->input->post('checked', true));
+
+ if ($id ?? '' != '') {
+ $this->cron_model->set_cron_enabled($id, $cron_enabled);
+ $data['success'] = 1;
+ } else {
+ $data['success'] = 0;
+ $data['flashdata'] = 'Not allowed';
+ }
+ echo json_encode($data);
+ }
+
+ public function fetchCrons() {
+ $hres = [];
+ $result = $this->cron_model->get_crons();
+
+ foreach ($result as $cron) {
+ $single = (object) [];
+ $single->cron_id = $cron->id;
+ $single->cron_description = $cron->description;
+ $single->cron_status = $this->cronStatus2html($cron->enabled, $cron->status);
+ $single->cron_expression = $this->cronExpression2html($cron->expression);
+ $single->cron_last_run = $cron->last_run ?? 'never';
+ $single->cron_next_run = ($cron->enabled == '1') ? ($cron->next_run ?? 'calculating..') : 'never';
+ $single->cron_edit = $this->cronEdit2html($cron->id);
+ $single->cron_enabled = $this->cronEnabled2html($cron->id, $cron->enabled);
+ array_push($hres, $single);
+ }
+ echo json_encode($hres);
+ }
+
+ private function cronStatus2html($enabled, $status) {
+ if ($enabled == '1') {
+ if ($status == 'healthy') {
+ $htmlret = 'healthy ';
+ } else {
+ $htmlret = '' . $status . ' ';
+ }
+ } else {
+ $htmlret = 'disabled ';
+ }
+ return $htmlret;
+ }
+
+ private function cronExpression2html($expression) {
+ $htmlret = '' . $expression . '
';
+ return $htmlret;
+ }
+
+ private function cronEdit2html($id) {
+ $htmlret = ' ';
+ return $htmlret;
+ }
+
+ private function cronEnabled2html($id, $enabled) {
+ if ($enabled == '1') {
+ $checked = 'checked';
+ } else {
+ $checked = '';
+ }
+ $htmlret = '
';
+ return $htmlret;
+ }
+
+ private function get_mastercron_status() {
+ $warning_timelimit_seconds = 120; // yellow - warning please check
+ $error_timelimit_seconds = 600; // red - "not running"
+
+ $result = array();
+
+ $last_run = $this->optionslib->get_option('mastercron_last_run') ?? null;
+
+ if ($last_run != null) {
+ $timestamp_last_run = DateTime::createFromFormat('Ymd H:i:s', $last_run, new DateTimeZone('UTC'));
+ $now = new DateTime();
+ $diff = $now->getTimestamp() - $timestamp_last_run->getTimestamp();
+
+ if ($diff >= 0 && $diff <= $warning_timelimit_seconds) {
+ $result['status'] = 'OK';
+ $result['status_class'] = 'success';
+ } else {
+ if ($diff <= $error_timelimit_seconds) {
+ $result['status'] = 'Last run occurred more than ' . $warning_timelimit_seconds . ' seconds ago. Please check your master cron! It should run every minute (* * * * *).';
+ $result['status_class'] = 'warning';
+ } else {
+ $result['status'] = 'Last run occurred more than ' . ($error_timelimit_seconds / 60) . ' minutes ago. Seems like your Mastercron isn\'t running! It should run every minute (* * * * *).';
+ $result['status_class'] = 'danger';
+ }
+ }
+ } else {
+ $result['status'] = 'Not running';
+ $result['status_class'] = 'danger';
+ }
+
+ return $result;
+ }
+
+}
diff --git a/application/controllers/Debug.php b/application/controllers/Debug.php
index 2e06ccf877..8857c7b4de 100644
--- a/application/controllers/Debug.php
+++ b/application/controllers/Debug.php
@@ -21,6 +21,7 @@ public function index() {
$this->load->model('Logbook_model');
$this->load->model('Debug_model');
$this->load->model('Stations');
+ $this->load->model('cron_model');
$footerData = [];
$footerData['scripts'] = ['assets/js/sections/debug.js'];
@@ -62,6 +63,14 @@ public function index() {
$data['userdata_status'] = $userdata_status;
}
+ $data['dxcc_update'] = $this->cron_model->cron('update_dxcc')->row();
+ $data['dok_update'] = $this->cron_model->cron('update_update_dok')->row();
+ $data['lotw_user_update'] = $this->cron_model->cron('update_lotw_users')->row();
+ $data['pota_update'] = $this->cron_model->cron('update_update_pota')->row();
+ $data['scp_update'] = $this->cron_model->cron('update_update_clublog_scp')->row();
+ $data['sota_update'] = $this->cron_model->cron('update_update_sota')->row();
+ $data['wwff_update'] = $this->cron_model->cron('update_update_wwff')->row();
+
$data['page_title'] = "Debug";
$this->load->view('interface_assets/header', $data);
diff --git a/application/controllers/Eqsl.php b/application/controllers/Eqsl.php
index a9df209298..790a929d2c 100644
--- a/application/controllers/Eqsl.php
+++ b/application/controllers/Eqsl.php
@@ -713,6 +713,11 @@ public function mark_all_sent() {
* Used for CRON job
*/
public function sync() {
+
+ // set the last run in cron table for the correct cron id
+ $this->load->model('cron_model');
+ $this->cron_model->set_last_run($this->router->class.'_'.$this->router->method);
+
ini_set('memory_limit', '-1');
set_time_limit(0);
$this->load->model('eqslmethods_model');
diff --git a/application/controllers/Hrdlog.php b/application/controllers/Hrdlog.php
index 2b3182b49f..0a0b09bdac 100644
--- a/application/controllers/Hrdlog.php
+++ b/application/controllers/Hrdlog.php
@@ -25,6 +25,10 @@ function __construct()
public function upload() {
$this->setOptions();
+ // set the last run in cron table for the correct cron id
+ $this->load->model('cron_model');
+ $this->cron_model->set_last_run($this->router->class.'_'.$this->router->method);
+
$this->load->model('logbook_model');
$station_ids = $this->logbook_model->get_station_id_with_hrdlog_code();
diff --git a/application/controllers/Lotw.php b/application/controllers/Lotw.php
index eb2eba33f0..2bae0a643c 100644
--- a/application/controllers/Lotw.php
+++ b/application/controllers/Lotw.php
@@ -198,6 +198,10 @@ public function lotw_upload() {
echo "You must install php OpenSSL for LoTW functions to work";
}
+ // set the last run in cron table for the correct cron id
+ $this->load->model('cron_model');
+ $this->cron_model->set_last_run($this->router->class.'_'.$this->router->method);
+
// Get Station Profile Data
$this->load->model('Stations');
diff --git a/application/controllers/Qrz.php b/application/controllers/Qrz.php
index 6eac702535..2f2ff77f7f 100644
--- a/application/controllers/Qrz.php
+++ b/application/controllers/Qrz.php
@@ -69,6 +69,10 @@ public function qrz_apitest() {
public function upload() {
$this->setOptions();
+ // set the last run in cron table for the correct cron id
+ $this->load->model('cron_model');
+ $this->cron_model->set_last_run($this->router->class.'_'.$this->router->method);
+
$this->load->model('logbook_model');
$station_ids = $this->logbook_model->get_station_id_with_qrz_api();
@@ -260,6 +264,8 @@ function download($user_id_to_load = null, $lastqrz = null, $show_views = false)
$this->load->model('user_model');
$this->load->model('logbook_model');
+ $this->load->model('cron_model');
+ $this->cron_model->set_last_run($this->router->class.'_'.$this->router->method);
$api_keys = $this->logbook_model->get_qrz_apikeys();
diff --git a/application/controllers/Update.php b/application/controllers/Update.php
index 542a4e3808..d0c4aceceb 100644
--- a/application/controllers/Update.php
+++ b/application/controllers/Update.php
@@ -20,6 +20,9 @@ function __construct()
public function index()
{
+ $this->load->model('user_model');
+ if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('notice', 'You\'re not allowed to do that!'); redirect('dashboard'); }
+
$data['page_title'] = "Updates";
$this->load->view('interface_assets/header', $data);
$this->load->view('update/index');
@@ -176,6 +179,11 @@ public function dxcc_prefixes() {
// Updates the DXCC & Exceptions from the Club Log Cty.xml file.
public function dxcc() {
+
+ // set the last run in cron table for the correct cron id
+ $this->load->model('cron_model');
+ $this->cron_model->set_last_run($this->router->class.'_'.$this->router->method);
+
$this->update_status("Downloading file");
// give it 10 minutes...
@@ -236,9 +244,6 @@ public function update_status($done=""){
$html .= "Dxcc Prefixes: ".$this->db->count_all('dxcc_prefixes')." ";
} else {
$html = $done.".... ";
- $datetime = new DateTime("now", new DateTimeZone('UTC'));
- $datetime = $datetime->format('Ymd h:i');
- $this->optionslib->update('dxcc_clublog_update', $datetime , 'no');
}
file_put_contents($this->make_update_path("status.html"), $html);
@@ -302,6 +307,11 @@ public function check_missing_grid($all = false){
}
public function update_clublog_scp() {
+
+ // set the last run in cron table for the correct cron id
+ $this->load->model('cron_model');
+ $this->cron_model->set_last_run($this->router->class.'_'.$this->router->method);
+
$strFile = $this->make_update_path("clublog_scp.txt");
$url = "https://cdn.clublog.org/clublog.scp.gz";
set_time_limit(300);
@@ -320,9 +330,6 @@ public function update_clublog_scp() {
if ($nCount > 0)
{
echo "DONE: " . number_format($nCount) . " callsigns loaded";
- $datetime = new DateTime("now", new DateTimeZone('UTC'));
- $datetime = $datetime->format('Ymd h:i');
- $this->optionslib->update('scp_update', $datetime , 'no');
} else {
echo "FAILED: Empty file";
}
@@ -352,6 +359,11 @@ public function download_lotw_users() {
}
public function lotw_users() {
+
+ // set the last run in cron table for the correct cron id
+ $this->load->model('cron_model');
+ $this->cron_model->set_last_run($this->router->class.'_'.$this->router->method);
+
$mtime = microtime();
$mtime = explode(" ",$mtime);
$mtime = $mtime[1] + $mtime[0];
@@ -390,9 +402,6 @@ public function lotw_users() {
$totaltime = ($endtime - $starttime);
echo "This page was created in ".$totaltime." seconds ";
echo "Records inserted: " . $i . " ";
- $datetime = new DateTime("now", new DateTimeZone('UTC'));
- $datetime = $datetime->format('Ymd h:i');
- $this->optionslib->update('lotw_users_update', $datetime , 'no');
}
public function lotw_check() {
@@ -412,6 +421,11 @@ public function lotw_check() {
* Used for autoupdating the DOK file which is used in the QSO entry dialog for autocompletion.
*/
public function update_dok() {
+
+ // set the last run in cron table for the correct cron id
+ $this->load->model('cron_model');
+ $this->cron_model->set_last_run($this->router->class.'_'.$this->router->method);
+
$contents = file_get_contents('https://www.df2et.de/cqrlog/dok_and_sdok.txt', true);
if($contents === FALSE) {
@@ -424,9 +438,6 @@ public function update_dok() {
if ($nCount > 0)
{
echo "DONE: " . number_format($nCount) . " DOKs and SDOKs saved";
- $datetime = new DateTime("now", new DateTimeZone('UTC'));
- $datetime = $datetime->format('Ymd h:i');
- $this->optionslib->update('dok_file_update', $datetime , 'no');
} else {
echo"FAILED: Empty file";
}
@@ -440,6 +451,11 @@ public function update_dok() {
* Used for autoupdating the SOTA file which is used in the QSO entry dialog for autocompletion.
*/
public function update_sota() {
+
+ // set the last run in cron table for the correct cron id
+ $this->load->model('cron_model');
+ $this->cron_model->set_last_run($this->router->class.'_'.$this->router->method);
+
$csvfile = 'https://www.sotadata.org.uk/summitslist.csv';
$sotafile = './assets/json/sota.txt';
@@ -474,9 +490,6 @@ public function update_sota() {
if ($nCount > 0)
{
echo "DONE: " . number_format($nCount) . " SOTA's saved";
- $datetime = new DateTime("now", new DateTimeZone('UTC'));
- $datetime = $datetime->format('Ymd h:i');
- $this->optionslib->update('sota_file_update', $datetime , 'no');
} else {
echo"FAILED: Empty file";
}
@@ -486,6 +499,11 @@ public function update_sota() {
* Pulls the WWFF directory for autocompletion in QSO dialogs
*/
public function update_wwff() {
+
+ // set the last run in cron table for the correct cron id
+ $this->load->model('cron_model');
+ $this->cron_model->set_last_run($this->router->class.'_'.$this->router->method);
+
$csvfile = 'https://wwff.co/wwff-data/wwff_directory.csv';
$wwfffile = './assets/json/wwff.txt';
@@ -524,15 +542,17 @@ public function update_wwff() {
if ($nCount > 0)
{
echo "DONE: " . number_format($nCount) . " WWFF's saved";
- $datetime = new DateTime("now", new DateTimeZone('UTC'));
- $datetime = $datetime->format('Ymd h:i');
- $this->optionslib->update('wwff_file_update', $datetime , 'no');
} else {
echo"FAILED: Empty file";
}
}
public function update_pota() {
+
+ // set the last run in cron table for the correct cron id
+ $this->load->model('cron_model');
+ $this->cron_model->set_last_run($this->router->class.'_'.$this->router->method);
+
$csvfile = 'https://pota.app/all_parks.csv';
$potafile = './assets/json/pota.txt';
@@ -570,9 +590,6 @@ public function update_pota() {
if ($nCount > 0)
{
echo "DONE: " . number_format($nCount) . " POTA's saved";
- $datetime = new DateTime("now", new DateTimeZone('UTC'));
- $datetime = $datetime->format('Ymd h:i');
- $this->optionslib->update('pota_file_update', $datetime , 'no');
} else {
echo"FAILED: Empty file";
}
diff --git a/application/libraries/CronExpression.php b/application/libraries/CronExpression.php
new file mode 100644
index 0000000000..439f74883c
--- /dev/null
+++ b/application/libraries/CronExpression.php
@@ -0,0 +1,424 @@
+ 0,
+ 'mon' => 1,
+ 'tue' => 2,
+ 'wed' => 3,
+ 'thu' => 4,
+ 'fri' => 5,
+ 'sat' => 6
+ ];
+
+ /**
+ * Month name look-up table
+ */
+ private const MONTH_NAMES = [
+ 'jan' => 1,
+ 'feb' => 2,
+ 'mar' => 3,
+ 'apr' => 4,
+ 'may' => 5,
+ 'jun' => 6,
+ 'jul' => 7,
+ 'aug' => 8,
+ 'sep' => 9,
+ 'oct' => 10,
+ 'nov' => 11,
+ 'dec' => 12
+ ];
+
+ /**
+ * Value boundaries
+ */
+ private const VALUE_BOUNDARIES = [
+ 0 => [
+ 'min' => 0,
+ 'max' => 59,
+ 'mod' => 1
+ ],
+ 1 => [
+ 'min' => 0,
+ 'max' => 23,
+ 'mod' => 1
+ ],
+ 2 => [
+ 'min' => 1,
+ 'max' => 31,
+ 'mod' => 1
+ ],
+ 3 => [
+ 'min' => 1,
+ 'max' => 12,
+ 'mod' => 1
+ ],
+ 4 => [
+ 'min' => 0,
+ 'max' => 7,
+ 'mod' => 0
+ ]
+ ];
+
+ /**
+ * @var DateTimeZone|null
+ */
+ protected readonly ?DateTimeZone $timeZone;
+
+ /**
+ * @var array|null
+ */
+ protected readonly ?array $registers;
+
+ /**
+ * @var string
+ */
+ protected readonly string $expression;
+
+ /**
+ * @param string $expression a cron expression, e.g. "* * * * *"
+ * @param DateTimeZone|null $timeZone time zone objectstring $expression, DateTimeZone $timeZone = null
+ */
+ public function __construct($data) {
+ $this->timeZone = $data['timeZone'];
+ $this->expression = $data['expression'];
+
+ try {
+ $this->registers = $this->parse($data['expression']);
+ } catch (Exception $e) {
+ $this->registers = null;
+ }
+ }
+
+ /**
+ * Whether current cron expression has been parsed successfully
+ *
+ * @return bool
+ */
+ public function isValid(): bool {
+ return null !== $this->registers;
+ }
+
+ /**
+ * Match either "now", a given date/time object or a timestamp against current cron expression
+ *
+ * @param mixed $when a DateTime object, a timestamp (int), or "now" if not set
+ * @return bool
+ * @throws Exception
+ */
+ public function isMatching($when = null): bool {
+ if (false === ($when instanceof DateTimeInterface)) {
+ $when = (new DateTime())->setTimestamp($when === null ? time() : $when);
+ }
+
+ if ($this->timeZone !== null) {
+ $when->setTimezone($this->timeZone);
+ }
+
+ return $this->isValid() && $this->match(sscanf($when->format('i G j n w'), '%d %d %d %d %d'));
+ }
+
+ /**
+ * Calculate next matching timestamp
+ *
+ * @param mixed $start a DateTime object, a timestamp (int) or "now" if not set
+ * @return int|bool next matching timestamp, or false on error
+ * @throws Exception
+ */
+ public function getNext($start = null) {
+ if ($this->isValid()) {
+ $next = $this->toDateTime($start);
+
+ do {
+ $pos = sscanf($next->format('i G j n Y w'), '%d %d %d %d %d %d');
+ } while ($this->increase($next, $pos));
+
+ return $next->getTimestamp();
+ }
+
+ return false;
+ }
+
+ /**
+ * @param mixed $start a DateTime object, a timestamp (int) or "now" if not set
+ * @return DateTime
+ */
+ private function toDateTime($start): DateTime {
+ if ($start instanceof DateTimeInterface) {
+ $next = $start;
+ } elseif ((int)$start > 0) {
+ $next = new DateTime('@' . $start);
+ } else {
+ $next = new DateTime('@' . time());
+ }
+
+ $next->setTimestamp($next->getTimeStamp() - $next->getTimeStamp() % 60);
+ $next->setTimezone($this->timeZone ?: new DateTimeZone(date_default_timezone_get()));
+
+ if ($this->isMatching($next)) {
+ $next->modify('+1 minute');
+ }
+
+ return $next;
+ }
+
+ /**
+ * Increases the timestamp in step sizes depending on which segment(s) of the cron pattern are matching.
+ * Returns FALSE if the cron pattern is matching and thus no further cycle is required.
+ *
+ * @param DateTimeInterface $next
+ * @param array $pos
+ * @return bool
+ */
+ private function increase(DateTimeInterface $next, array $pos): bool {
+ switch (true) {
+ case false === isset($this->registers[3][$pos[3]]):
+ // next month, reset day/hour/minute
+ $next->setTime(0, 0);
+ $next->setDate($pos[4], $pos[3], 1);
+ $next->modify('+1 month');
+ return true;
+
+ case false === (isset($this->registers[2][$pos[2]]) && isset($this->registers[4][$pos[5]])):
+ // next day, reset hour/minute
+ $next->setTime(0, 0);
+ $next->modify('+1 day');
+ return true;
+
+ case false === isset($this->registers[1][$pos[1]]):
+ // next hour, reset minute
+ $next->setTime($pos[1], 0);
+ $next->modify('+1 hour');
+ return true;
+
+ case false === isset($this->registers[0][$pos[0]]):
+ // next minute
+ $next->modify('+1 minute');
+ return true;
+
+ default:
+ // all segments are matching
+ return false;
+ }
+ }
+
+ /**
+ * @param array $segments
+ * @return bool
+ */
+ private function match(array $segments): bool {
+ foreach ($this->registers as $i => $item) {
+ if (isset($item[(int)$segments[$i]]) === false) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Parse whole cron expression
+ *
+ * @param string $expression
+ * @return array
+ * @throws Exception
+ */
+ private function parse(string $expression): array {
+ $segments = preg_split('/\s+/', trim($expression));
+
+ if (is_array($segments) && sizeof($segments) === 5) {
+ $registers = array_fill(0, 5, []);
+
+ foreach ($segments as $index => $segment) {
+ $this->parseSegment($registers[$index], $index, $segment);
+ }
+
+ $this->validateDate($registers);
+
+ if (isset($registers[4][7])) {
+ $registers[4][0] = true;
+ }
+
+ return $registers;
+ }
+
+ throw new Exception('invalid number of segments');
+ }
+
+ /**
+ * Parse one segment of a cron expression
+ *
+ * @param array $register
+ * @param int $index
+ * @param string $segment
+ * @throws Exception
+ */
+ private function parseSegment(array &$register, int $index, string $segment): void {
+ $allowed = [false, false, false, self::MONTH_NAMES, self::WEEKDAY_NAMES];
+
+ // month names, weekdays
+ if ($allowed[$index] !== false && isset($allowed[$index][strtolower($segment)])) {
+ // cannot be used together with lists or ranges
+ $register[$allowed[$index][strtolower($segment)]] = true;
+ } else {
+ // split up current segment into single elements, e.g. "1,5-7,*/2" => [ "1", "5-7", "*/2" ]
+ foreach (explode(',', $segment) as $element) {
+ $this->parseElement($register, $index, $element);
+ }
+ }
+ }
+
+ /**
+ * @param array $register
+ * @param int $index
+ * @param string $element
+ * @throws Exception
+ */
+ private function parseElement(array &$register, int $index, string $element): void {
+ $step = 1;
+ $segments = explode('/', $element);
+
+ if (sizeof($segments) > 1) {
+ $this->validateStepping($segments, $index);
+
+ $element = (string)$segments[0];
+ $step = (int)$segments[1];
+ }
+
+ if (is_numeric($element)) {
+ $this->validateValue($element, $index, $step);
+ $register[intval($element)] = true;
+ } else {
+ $this->parseRange($register, $index, $element, $step);
+ }
+ }
+
+ /**
+ * Parse range of values, e.g. "5-10"
+ *
+ * @param array $register
+ * @param int $index
+ * @param string $range
+ * @param int $stepping
+ * @throws Exception
+ */
+ private function parseRange(array &$register, int $index, string $range, int $stepping): void {
+ if ($range === '*') {
+ $rangeArr = [self::VALUE_BOUNDARIES[$index]['min'], self::VALUE_BOUNDARIES[$index]['max']];
+ } else {
+ $rangeArr = explode('-', $range);
+ }
+
+ $this->validateRange($rangeArr, $index);
+ $this->fillRange($register, $index, $rangeArr, $stepping);
+ }
+
+ /**
+ * @param array $register
+ * @param int $index
+ * @param array $range
+ * @param int $stepping
+ */
+ private function fillRange(array &$register, int $index, array $range, int $stepping): void {
+ $boundary = self::VALUE_BOUNDARIES[$index]['max'] + self::VALUE_BOUNDARIES[$index]['mod'];
+ $length = $range[1] - $range[0];
+
+ for ($i = 0; $i <= $length; $i += $stepping) {
+ $register[($range[0] + $i) % $boundary] = true;
+ }
+ }
+
+ /**
+ * Validate whether a given range of values exceeds allowed value boundaries
+ *
+ * @param array $range
+ * @param int $index
+ * @throws Exception
+ */
+ private function validateRange(array $range, int $index): void {
+ if (sizeof($range) !== 2) {
+ throw new Exception('invalid range notation');
+ }
+
+ foreach ($range as $value) {
+ $this->validateValue($value, $index);
+ }
+
+ if ($range[0] > $range[1]) {
+ throw new Exception('lower value in range is larger than upper value');
+ }
+ }
+
+ /**
+ * @param string $value
+ * @param int $index
+ * @param int $step
+ * @throws Exception
+ */
+ private function validateValue(string $value, int $index, int $step = 1): void {
+ if (false === ctype_digit($value)) {
+ throw new Exception('non-integer value');
+ }
+
+ if (
+ intval($value) < self::VALUE_BOUNDARIES[$index]['min'] ||
+ intval($value) > self::VALUE_BOUNDARIES[$index]['max']
+ ) {
+ throw new Exception('value out of boundary');
+ }
+
+ if ($step !== 1) {
+ throw new Exception('invalid combination of value and stepping notation');
+ }
+ }
+
+ /**
+ * @param array $segments
+ * @param int $index
+ * @throws Exception
+ */
+ private function validateStepping(array $segments, int $index): void {
+ if (sizeof($segments) !== 2) {
+ throw new Exception('invalid stepping notation');
+ }
+
+ if ((int)$segments[1] < 1 || (int)$segments[1] > self::VALUE_BOUNDARIES[$index]['max']) {
+ throw new Exception('stepping out of allowed range');
+ }
+ }
+
+ /**
+ * @param array $segments
+ * @throws Exception
+ */
+ private function validateDate(array $segments): void {
+ $year = date('Y');
+
+ for ($y = 0; $y < 27; $y++) {
+ foreach (array_keys($segments[3]) as $month) {
+ foreach (array_keys($segments[2]) as $day) {
+ if (false === checkdate($month, $day, $year + $y)) {
+ continue;
+ }
+
+ if (false === isset($segments[date('w', strtotime(sprintf('%d-%d-%d', $year + $y, $month, $day)))])) {
+ continue;
+ }
+
+ return;
+ }
+ }
+ }
+
+ throw new Exception('no date ever can match the given combination of day/month/weekday');
+ }
+}
diff --git a/application/migrations/196_cron_table.php b/application/migrations/196_cron_table.php
new file mode 100644
index 0000000000..f8b1dfce3d
--- /dev/null
+++ b/application/migrations/196_cron_table.php
@@ -0,0 +1,216 @@
+db->table_exists('cron')) {
+
+ // define the structure of the new cron table
+ $this->dbforge->add_field(array(
+ 'id' => array(
+ 'type' => 'VARCHAR',
+ 'constraint' => '255',
+ 'null' => FALSE,
+ ),
+ 'enabled' => array(
+ 'type' => 'TINYINT',
+ 'constraint' => '1',
+ 'null' => FALSE,
+ ),
+ 'status' => array(
+ 'type' => 'VARCHAR',
+ 'constraint' => '255',
+ 'null' => TRUE,
+ ),
+ 'description' => array(
+ 'type' => 'VARCHAR',
+ 'constraint' => '255',
+ 'null' => TRUE,
+ ),
+ 'function' => array(
+ 'type' => 'VARCHAR',
+ 'constraint' => '255',
+ 'null' => FALSE,
+ ),
+ 'expression' => array(
+ 'type' => 'VARCHAR',
+ 'constraint' => '100',
+ 'null' => TRUE,
+ ),
+ 'last_run' => array(
+ 'type' => 'TIMESTAMP',
+ 'null' => TRUE,
+ ),
+ 'next_run' => array(
+ 'type' => 'TIMESTAMP',
+ 'null' => TRUE,
+ ),
+ 'modified' => array(
+ 'type' => 'TIMESTAMP',
+ 'null' => TRUE,
+ ),
+ ));
+
+ // we set the key for the id, in this case the id is not numerical
+ $this->dbforge->add_key('id', TRUE);
+
+ // now we can create the new table
+ $this->dbforge->create_table('cron');
+
+ // to transfer data for the file updates we load the optionslib library
+ $this->load->library('OptionsLib');
+
+ // and we fill the table with the cronjobs
+ $data = array(
+ array(
+ 'id' => 'clublog_upload',
+ 'enabled' => '0',
+ 'status' => 'pending',
+ 'description' => 'Upload QSOs to Clublog',
+ 'function' => 'index.php/clublog/upload',
+ 'expression' => '3 */6 * * *',
+ 'last_run' => null,
+ 'next_run' => null
+ ),
+ array(
+ 'id' => 'lotw_lotw_upload',
+ 'enabled' => '0',
+ 'status' => 'pending',
+ 'description' => 'Upload QSOs to LoTW',
+ 'function' => 'index.php/lotw/lotw_upload',
+ 'expression' => '0 */1 * * *',
+ 'last_run' => null,
+ 'next_run' => null
+ ),
+ array(
+ 'id' => 'qrz_upload',
+ 'enabled' => '0',
+ 'status' => 'pending',
+ 'description' => 'Upload QSOs to QRZ',
+ 'function' => 'index.php/qrz/upload',
+ 'expression' => '6 */6 * * *',
+ 'last_run' => null,
+ 'next_run' => null
+ ),
+ array(
+ 'id' => 'qrz_download',
+ 'enabled' => '0',
+ 'status' => 'pending',
+ 'description' => 'Download QSOs from QRZ',
+ 'function' => 'index.php/qrz/download',
+ 'expression' => '18 */6 * * *',
+ 'last_run' => null,
+ 'next_run' => null
+ ),
+ array(
+ 'id' => 'hrdlog_upload',
+ 'enabled' => '0',
+ 'status' => 'pending',
+ 'description' => 'Upload QSOs to HRD',
+ 'function' => 'index.php/hrdlog/upload',
+ 'expression' => '12 */6 * * *',
+ 'last_run' => null,
+ 'next_run' => null
+ ),
+ array(
+ 'id' => 'eqsl_sync',
+ 'enabled' => '0',
+ 'status' => 'pending',
+ 'description' => 'Upload/download QSOs to/from Eqsl',
+ 'function' => 'index.php/eqsl/sync',
+ 'expression' => '9 */6 * * *',
+ 'last_run' => null,
+ 'next_run' => null
+ ),
+ array(
+ 'id' => 'update_lotw_users',
+ 'enabled' => '1',
+ 'status' => 'pending',
+ 'description' => 'Update LOTW Users Activity',
+ 'function' => 'index.php/update/lotw_users',
+ 'expression' => '10 1 * * 1',
+ 'last_run' => ($this->optionslib->get_option('lotw_users_update') ? date("Y-m-d H:i", strtotime($this->optionslib->get_option('lotw_users_update'))) : null),
+ 'next_run' => null
+ ),
+ array(
+ 'id' => 'update_update_clublog_scp',
+ 'enabled' => '1',
+ 'status' => 'pending',
+ 'description' => 'Update Clublog SCP Database File',
+ 'function' => 'index.php/update/update_clublog_scp',
+ 'expression' => '0 0 * * 0',
+ 'last_run' => ($this->optionslib->get_option('scp_update') ? date("Y-m-d H:i", strtotime($this->optionslib->get_option('scp_update'))) : null),
+ 'next_run' => null
+ ),
+ array(
+ 'id' => 'update_update_dok',
+ 'enabled' => '1',
+ 'status' => 'pending',
+ 'description' => 'Update DOK File',
+ 'function' => 'index.php/update/update_dok',
+ 'expression' => '0 0 1 * *',
+ 'last_run' => ($this->optionslib->get_option('dok_file_update') ? date("Y-m-d H:i", strtotime($this->optionslib->get_option('dok_file_update'))) : null),
+ 'next_run' => null
+ ),
+ array(
+ 'id' => 'update_update_sota',
+ 'enabled' => '1',
+ 'status' => 'pending',
+ 'description' => 'Update SOTA File',
+ 'function' => 'index.php/update/update_sota',
+ 'expression' => '5 0 1 * *',
+ 'last_run' => ($this->optionslib->get_option('sota_file_update') ? date("Y-m-d H:i", strtotime($this->optionslib->get_option('sota_file_update'))) : null),
+ 'next_run' => null
+ ),
+ array(
+ 'id' => 'update_update_wwff',
+ 'enabled' => '1',
+ 'status' => 'pending',
+ 'description' => 'Update WWFF File',
+ 'function' => 'index.php/update/update_wwff',
+ 'expression' => '10 0 1 * *',
+ 'last_run' => ($this->optionslib->get_option('wwff_file_update') ? date("Y-m-d H:i", strtotime($this->optionslib->get_option('wwff_file_update'))) : null),
+ 'next_run' => null
+ ),
+ array(
+ 'id' => 'update_update_pota',
+ 'enabled' => '1',
+ 'status' => 'pending',
+ 'description' => 'Update POTA File',
+ 'function' => 'index.php/update/update_pota',
+ 'expression' => '15 0 1 * *',
+ 'last_run' => ($this->optionslib->get_option('pota_file_update') ? date("Y-m-d H:i", strtotime($this->optionslib->get_option('pota_file_update'))) : null),
+ 'next_run' => null
+ ),
+ array(
+ 'id' => 'update_dxcc',
+ 'enabled' => '1',
+ 'status' => 'pending',
+ 'description' => 'Update DXCC data',
+ 'function' => 'index.php/update/dxcc',
+ 'expression' => '20 0 1 */2 *',
+ 'last_run' => ($this->optionslib->get_option('dxcc_clublog_update') ? date("Y-m-d H:i", strtotime($this->optionslib->get_option('dxcc_clublog_update'))) : null),
+ 'next_run' => null
+ ),
+ );
+ $this->db->insert_batch('cron', $data);
+
+ // since we transfered the source for the file update timestamps we don't need this options anymore
+ $this->db->delete('options', array('option_name' => 'lotw_users_update'));
+ $this->db->delete('options', array('option_name' => 'scp_update'));
+ $this->db->delete('options', array('option_name' => 'dok_file_update'));
+ $this->db->delete('options', array('option_name' => 'sota_file_update'));
+ $this->db->delete('options', array('option_name' => 'wwff_file_update'));
+ $this->db->delete('options', array('option_name' => 'pota_file_update'));
+ $this->db->delete('options', array('option_name' => 'dxcc_clublog_update'));
+ }
+ }
+
+ public function down() {
+
+ $this->dbforge->drop_table('cron');
+
+ }
+}
diff --git a/application/models/Cron_model.php b/application/models/Cron_model.php
new file mode 100644
index 0000000000..c616382f30
--- /dev/null
+++ b/application/models/Cron_model.php
@@ -0,0 +1,93 @@
+db->from('cron');
+
+ $results = array();
+
+ $results = $this->db->get()->result();
+
+ return $results;
+ }
+
+ // get details for a specific cron
+ function cron($id) {
+
+ $clean_id = $this->security->xss_clean($id);
+
+ $this->db->where('id', $clean_id);
+
+ return $this->db->get('cron');
+ }
+
+ // set the modified timestamp
+ function set_modified($cron) {
+ $data = array(
+ 'modified' => date('Y-m-d H:i:s')
+ );
+
+ $this->db->where('id', $cron);
+ $this->db->update('cron', $data);
+ }
+
+ // set a new status for the cron
+ function set_status($cron, $status) {
+ $data = array(
+ 'status' => $status
+ );
+
+ $this->db->where('id', $cron);
+ $this->db->update('cron', $data);
+ }
+
+ // set the last run
+ function set_last_run($cron) {
+ $data = array(
+ 'last_run' => date('Y-m-d H:i:s')
+ );
+
+ $this->db->where('id', $cron);
+ $this->db->update('cron', $data);
+ }
+
+ // set the calculated next run
+ function set_next_run($cron,$timestamp) {
+ $data = array(
+ 'next_run' => $timestamp
+ );
+
+ $this->db->where('id', $cron);
+ $this->db->update('cron', $data);
+ }
+
+ // set the cron enabled flag
+ function set_cron_enabled($cron, $cron_enabled) {
+ $data = array (
+ 'enabled' => ($cron_enabled === 'true' ? 1 : 0),
+ 'status' => ($cron_enabled === 'true' ? 'pending' : 'disabled'),
+ );
+
+ $this->db->where('id', $cron);
+ $this->db->update('cron', $data);
+
+ $this->set_modified($cron);
+ }
+
+ // set the edited details for a cron
+ function edit_cron($id, $description, $expression, $enabled) {
+
+ $data = array (
+ 'description' => $description,
+ 'expression' => $expression,
+ 'enabled' => ($enabled === 'true' ? 1 : 0)
+ );
+
+ $this->db->where('id', $id);
+ $this->db->update('cron', $data);
+
+ $this->set_modified($id);
+ }
+}
diff --git a/application/views/cron/edit.php b/application/views/cron/edit.php
new file mode 100644
index 0000000000..d69afe2c76
--- /dev/null
+++ b/application/views/cron/edit.php
@@ -0,0 +1,78 @@
+
\ No newline at end of file
diff --git a/application/views/cron/index.php b/application/views/cron/index.php
new file mode 100644
index 0000000000..964f4dd19a
--- /dev/null
+++ b/application/views/cron/index.php
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ The Cron Manager assists the administrator in managing cron jobs without requiring CLI access.
+
+
+ To execute cron jobs based on the data below, remove all old cron jobs and create a new one:
+
+
+
* * * * * curl --silent index.php/cron/run &>/dev/null
+
+
+
+
+ Status Master-Cron:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Your Mastercron isn't running. Copy the cron above to a external cron service or into your server's cron to use this cron manager.
+
On a basic linux server with shell access use this command to edit your crons:
crontab -e
+
+
+
+
+
\ No newline at end of file
diff --git a/application/views/debug/index.php b/application/views/debug/index.php
index 67ef0ef663..75aeae2b5c 100644
--- a/application/views/debug/index.php
+++ b/application/views/debug/index.php
@@ -462,38 +462,38 @@
DXCC update from Club Log
- optionslib->get_option('dxcc_clublog_update') ?? '') == '' ? '' : date($custom_date_format, strtotime($this->optionslib->get_option('dxcc_clublog_update') ?? '')) . ' ' . date("h:i", strtotime($this->optionslib->get_option('dxcc_clublog_update') ?? ''))) ?>
+ last_run ?? 'never'; ?>
Update
DOK file download
- optionslib->get_option('dok_file_update') ?? '') == '' ? '' : date($custom_date_format, strtotime($this->optionslib->get_option('dok_file_update') ?? '')) . ' ' . date("h:i", strtotime($this->optionslib->get_option('dok_file_update') ?? ''))) ?>
+ last_run ?? 'never'; ?>
Update
LoTW users download
- optionslib->get_option('lotw_users_update') ?? '') == '' ? '' : date($custom_date_format, strtotime($this->optionslib->get_option('lotw_users_update') ?? '')) . ' ' . date("h:i", strtotime($this->optionslib->get_option('lotw_users_update') ?? ''))) ?>
+ last_run ?? 'never'; ?>
Update
POTA file download
- optionslib->get_option('pota_file_update') ?? '') == '' ? '' : date($custom_date_format, strtotime($this->optionslib->get_option('pota_file_update') ?? '')) . ' ' . date("h:i", strtotime($this->optionslib->get_option('pota_file_update') ?? ''))) ?>
+ last_run ?? 'never'; ?>
Update
SCP file download
- optionslib->get_option('scp_update') ?? '') == '' ? '' : date($custom_date_format, strtotime($this->optionslib->get_option('scp_update') ?? '')) . ' ' . date("h:i", strtotime($this->optionslib->get_option('scp_update') ?? ''))) ?>
+ last_run ?? 'never'; ?>
Update
SOTA file download
- optionslib->get_option('sota_file_update') ?? '') == '' ? '' : date($custom_date_format, strtotime($this->optionslib->get_option('sota_file_update') ?? '')) . ' ' . date("h:i", strtotime($this->optionslib->get_option('sota_file_update') ?? ''))) ?>
+ last_run ?? 'never'; ?>
Update
WWFF file download
- optionslib->get_option('wwff_file_update') ?? '') == '' ? '' : date($custom_date_format, strtotime($this->optionslib->get_option('wwff_file_update') ?? '')) . ' ' . date("h:i", strtotime($this->optionslib->get_option('wwff_file_update') ?? ''))) ?>
+ last_run ?? 'never'; ?>
Update
diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php
index e3d814c96a..0882f62f75 100644
--- a/application/views/interface_assets/footer.php
+++ b/application/views/interface_assets/footer.php
@@ -106,6 +106,11 @@ function getDataTablesLanguageUrl() {
+
+uri->segment(1) == "cron") { ?>
+
+
+
uri->segment(1) == "options") { ?>