From 069f587c1d496f8b4c6a4c295a3083fbfa889fbc Mon Sep 17 00:00:00 2001 From: johnnyq Date: Wed, 26 Aug 2026 23:38:23 -0400 Subject: [PATCH] Implemented Check for updates which removs the last shell exec from web ui, and reimplemented db_update switch to update_cli.php --- admin/database_updates/2.7.3.php | 26 ++++ admin/debug.php | 41 ++---- admin/post/update.php | 26 ++++ admin/update.php | 142 +++++++++++------- cron/nightly_tasks.php | 22 ++- cron/update_check.php | 81 +++++++++++ db.sql | 5 +- functions/app.php | 237 +++++++++++++++++++++++++++++-- includes/cron_jobs.php | 8 ++ js/auto_reload.js | 34 +++++ scripts/restore_cli.php | 2 +- scripts/update_cli.php | 49 +++++-- setup/index.php | 32 ++--- 13 files changed, 573 insertions(+), 132 deletions(-) create mode 100644 admin/database_updates/2.7.3.php create mode 100644 cron/update_check.php create mode 100644 js/auto_reload.js diff --git a/admin/database_updates/2.7.3.php b/admin/database_updates/2.7.3.php new file mode 100644 index 000000000..846ab947e --- /dev/null +++ b/admin/database_updates/2.7.3.php @@ -0,0 +1,26 @@ + Update no longer runs git itself. cron/update_check.php does the fetch + // on its own schedule and parks the answer here; the page and the nightly notification + // both read these rather than shelling out in a web request. + // + // config_update_checked_at is written only after a check SUCCEEDS, so "last checked" + // never claims freshness for a reading that a failed fetch left stale. The cron_jobs row + // holds the attempt time and the error. + + mysqli_query($mysqli, "ALTER TABLE `settings` + ADD COLUMN IF NOT EXISTS `config_update_latest_commit` varchar(40) DEFAULT NULL, + ADD COLUMN IF NOT EXISTS `config_update_pending_commits` text DEFAULT NULL, + ADD COLUMN IF NOT EXISTS `config_update_checked_at` datetime DEFAULT NULL"); + + // Seeded here as well as by the dispatcher, which only creates rows on its next pass - + // without this, a Check Now pressed in the minutes after an upgrade would have no row to + // set cron_job_run_now on and would wait for that pass instead of starting. + mysqli_query($mysqli, "INSERT IGNORE INTO cron_jobs SET cron_job_name = 'update_check', cron_job_enabled = 1, cron_job_schedule = 'Daily', cron_job_daily_at = '02:30'"); diff --git a/admin/debug.php b/admin/debug.php index f4147e4a5..d6ac0694c 100644 --- a/admin/debug.php +++ b/admin/debug.php @@ -6,11 +6,10 @@ require_once "../config.php"; $checks = []; -// Execute the git command to get the latest commit hash -$commitHash = shell_exec('git log -1 --format=%H'); +// Read straight out of .git - no shell needed, and it still answers on a hardened host +$commitHash = gitCurrentCommit(); -// Get branch info -$gitBranch = shell_exec('git rev-parse --abbrev-ref HEAD'); +$gitBranch = gitCurrentBranch(); // Section: System Information $systemInfo = []; @@ -146,27 +145,15 @@ $phpConfig[] = [ // Section: Shell Commands $shellCommands = []; -if ($shell_exec_enabled) { - $commands = ['git']; - - foreach ($commands as $command) { - $which = trim(shell_exec("which $command 2>/dev/null")); - $exists = !empty($which); - $shellCommands[] = [ - 'name' => "Command '$command' available", - 'passed' => $exists, - 'value' => $exists ? $which : 'Not Found', - ]; - } -} else { - // If shell_exec is disabled, mark commands as unavailable - foreach (['git'] as $command) { - $shellCommands[] = [ - 'name' => "Command '$command' available", - 'passed' => false, - 'value' => 'shell_exec Disabled', - ]; - } +// Located by walking PATH rather than by running `which`, so this reports the truth on a +// host with shell_exec disabled instead of reporting the host's php.ini back at itself +foreach (['git'] as $command) { + $path = commandPath($command); + $shellCommands[] = [ + 'name' => "Command '$command' available", + 'passed' => $path !== '', + 'value' => $path !== '' ? $path : 'Not Found', + ]; } // Section: SSL Checks @@ -531,11 +518,11 @@ $mysqli->close(); Current Code Commit - + Current Branch - + diff --git a/admin/post/update.php b/admin/post/update.php index 625473754..a65770330 100644 --- a/admin/post/update.php +++ b/admin/post/update.php @@ -14,6 +14,32 @@ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed"); * cron/app_update.php acts on, and cron_job_run_now is what gets the dispatcher to look * before the job's own schedule comes round. */ +/* + * Check Now. The check runs git fetch, so it belongs on the command line for the same reason + * the update does; this only asks the dispatcher to bring the job forward. run_now works on a + * disabled job too, so turning the daily check off does not take the button with it. + */ +if (isset($_GET['check_update'])) { + + validateCSRFToken(); + + enforceAdminPermission(); + + if (!settingsColumnExists($mysqli, 'config_update_latest_commit')) { + flashAlert("Checking needs a schema change this install has not caught up with yet - run php scripts/update_cli.php from a shell once, and it will work from then on.", 'error'); + redirect(); + } + + mysqli_query($mysqli, "UPDATE cron_jobs SET cron_job_run_now = 1 WHERE cron_job_name = 'update_check'"); + + logAudit("App", "Update", "$session_name asked cron to check for updates"); + + flashAlert("Checking for updates - cron will run the check within a minute."); + + redirect(); + +} + if (isset($_GET['queue_update'])) { validateCSRFToken(); diff --git a/admin/update.php b/admin/update.php index 3af7b40f2..84fabb64d 100644 --- a/admin/update.php +++ b/admin/update.php @@ -4,52 +4,51 @@ require_once "includes/inc_all_admin.php"; require_once "../includes/database_version.php"; $repo_branch = getRepoBranch(); -$remote_ref = escapeshellarg("origin/$repo_branch"); /* - * The web server never applies an application update - it queues one and cron runs it. The - * shell gate below therefore only decides whether this page can READ the git remote to say - * an update is waiting; where PHP cannot run one - shared hosting, a hardened php.ini, an - * FPM pool locked down while the command line is not - the page offers the queue blind, - * which is harmless when there is nothing to fetch. The database half is plain PHP and - * works everywhere. + * This page does not run git. cron/update_check.php does the fetch on its own schedule and + * stores what it found; everything below is a read of that, plus .git for the local commit. + * Check Now asks the dispatcher for a run rather than checking inside the request, so the + * page works the same on a host whose web PHP cannot run external commands at all. */ -$shell_available = shellCommandsAvailable(); +$current_version = gitCurrentCommit(); -$current_version = ''; -$fetch_ok = true; -$updates = null; +// The stored answer arrives with a migration, and this page has to render on an install +// whose files are newer than its schema +$update_check_available = settingsColumnExists($mysqli, 'config_update_latest_commit'); -// Commits sitting between this working tree and the remote branch. Fields are separated -// by \x1f rather than having git build the table markup, because a commit subject comes -// from outside this install and used to reach the page as unescaped HTML. +$latest_version = ''; +$update_checked_at = null; $pending_commits = []; +$check_job = null; -if ($shell_available) { +if ($update_check_available) { - $updates = checkForUpdates(); + $update_check_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_update_latest_commit, config_update_pending_commits, config_update_checked_at FROM settings WHERE company_id = 1")); - $current_version = $updates->current_version; - $fetch_ok = $updates->result === 0; + $latest_version = (string) ($update_check_row['config_update_latest_commit'] ?? ''); + $update_checked_at = $update_check_row['config_update_checked_at'] ?? null; - $git_log = shell_exec("git log HEAD..$remote_ref --pretty=format:'%h%x1f%ar%x1f%s'"); + // Stored as JSON by the job: [[short hash, ISO date, subject], ...]. Rebuilt element by + // element rather than trusted wholesale - it is the shape the table indexes into + $stored_commits = json_decode((string) ($update_check_row['config_update_pending_commits'] ?? ''), true); - foreach (explode("\n", trim((string) $git_log)) as $commit_line) { - - if ($commit_line === '') { - continue; + if (is_array($stored_commits)) { + foreach ($stored_commits as $stored_commit) { + if (is_array($stored_commit) && count($stored_commit) === 3) { + $pending_commits[] = array_values($stored_commit); + } } - - $commit_fields = explode("\x1f", $commit_line, 3); - - if (count($commit_fields) === 3) { - $pending_commits[] = $commit_fields; - } - } + $check_job = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT cron_job_run_now, cron_job_last_error, cron_job_last_error_at FROM cron_jobs WHERE cron_job_name = 'update_check' LIMIT 1")); + } +// The dispatcher clears run_now inside the same UPDATE that claims the job, so this is true +// for exactly as long as the request is outstanding +$check_in_progress = !empty($check_job['cron_job_run_now']); + /* * The queue. Maintenance > Update writes config_update_queued_at and asks the dispatcher for * an immediate run; cron/app_update.php takes the request and runs scripts/update_cli.php in @@ -84,7 +83,12 @@ $cron_is_running = $update_queue_available // version_compare, not > - "2.6.10" is less than "2.6.9" as a plain string comparison, so // the plain comparison silently stops offering database updates once a minor reaches 10. $db_update_available = version_compare(LATEST_DATABASE_VERSION, CURRENT_DATABASE_VERSION, '>'); -$app_update_available = !empty($pending_commits); + +// Derived from the two commits, not from the list: a force-push or a local commit leaves the +// working tree at a different place with nothing to list, and that is still "not up to date" +$app_update_available = $latest_version !== '' + && $current_version !== '' + && $latest_version !== $current_version; ?> @@ -94,26 +98,33 @@ $app_update_available = !empty($pending_commits);
- +
-
Cannot reach the Git remote
- ITFlow updates itself with Git, so nothing below is current until this is fixed. - output)) { ?> -
output)) ?>
- - Check that Git is installed, that the remote is reachable from this server, and that the web server - user can write to the ITFlow directory. The - forum can help - include - your PHP error log and the output above. +
The last update check did not finish
+ Anything below is from the check before it, so it may be out of date. +
+ Recorded . Check that Git is installed + and that the remote is reachable from this server, then clear the error from + Maintenance > Cron. The + forum can help - include the + output above.
- -
-
This server cannot check for updates
- PHP here has exec and shell_exec disabled, so this page cannot read the Git - remote to tell you whether one is waiting. Queueing still works - cron runs the update from the command - line, which is usually not restricted the same way. Database updates are plain PHP and are unaffected. + + +
+ + Checking for updates. + + Cron runs the check within a minute; this page refreshes itself. + + Nothing is going to pick it up - cron has not checked in + recently or the master switch is off. See + Maintenance > Cron. +
@@ -144,6 +155,25 @@ $app_update_available = !empty($pending_commits);
+ +

+ + + Last checked + (). + + Never checked. + + + Check now + + Checking… + + +

+
An update was queued at @@ -168,7 +198,7 @@ $app_update_available = !empty($pending_commits);
- +
@@ -198,7 +228,7 @@ $app_update_available = !empty($pending_commits); together under one button rather than as separate steps. The db_update_available arm of the condition matters: a schema behind its code with nothing to pull is exactly the state that needs queueing, and without it the button would not draw. */ ?> - +
Pending
@@ -208,11 +238,11 @@ $app_update_available = !empty($pending_commits); commit behind .

- +

- Application files: this server cannot check - , so there may or may not be anything - waiting. Queueing an update when there is nothing to do is harmless. + Application files: this install has not checked + yet, so there may or may not be + anything waiting. Queueing an update when there is nothing to do is harmless.

@@ -272,7 +302,9 @@ $app_update_available = !empty($pending_commits); - + + @@ -284,6 +316,8 @@ $app_update_available = !empty($pending_commits);
+ + 0 || $config_telemetry == 2) { - $current_version = exec("git rev-parse HEAD"); + $current_version = gitCurrentCommit(); // Client Count $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('client_id') AS num FROM clients")); @@ -1403,13 +1403,23 @@ if ($config_telemetry > 0 || $config_telemetry == 2) { // Fetch Updates -$updates = checkForUpdates(); +/* + * The check itself is cron/update_check.php now - read what it stored rather than running a + * second git fetch here. Nothing to say until that job has run once, which is the same + * position this was in when a fetch failed. + */ +if (settingsColumnExists($mysqli, 'config_update_latest_commit')) { -$update_message = $updates->update_message; + $update_check_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_update_latest_commit FROM settings WHERE company_id = 1")); + + $latest_version = (string) ($update_check_row['config_update_latest_commit'] ?? ''); + $current_version = gitCurrentCommit(); + + if ($latest_version !== '' && $current_version !== '' && $latest_version !== $current_version) { + // Send Alert to inform Updates Available + appNotify("Update", "New Updates are Available [$latest_version]", "/admin/update.php"); + } -if ($updates->current_version !== $updates->latest_version) { - // Send Alert to inform Updates Available - appNotify("Update", "$update_message", "/admin/update.php"); } diff --git a/cron/update_check.php b/cron/update_check.php new file mode 100644 index 000000000..f5ade90f3 --- /dev/null +++ b/cron/update_check.php @@ -0,0 +1,81 @@ + Update sets cron_job_run_now on this job rather than checking + * inside the request - same mechanism as Queue Update, and the page shows "Checking" until + * the dispatcher consumes the flag. + * + * Safe to run twice: it reads and overwrites, and changes nothing outside the three settings + * columns it owns. + */ + +// Set working directory to the directory this cron script lives at. +chdir(dirname(__FILE__)); + +// Ensure we're running from command line +if (php_sapi_name() !== 'cli') { + die("This script must be run from the command line.\n"); +} + +// Prevent overlapping runs of this script +$cron_lock_script = __FILE__; +require_once "includes/cron_lock.php"; + +require_once "../config.php"; + +// Set Timezone +require_once "../includes/inc_set_timezone.php"; +require_once "../functions.php"; + +$update_check_settings = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_enable_cron FROM settings WHERE company_id = 1")); + +$config_enable_cron = intval($update_check_settings['config_enable_cron'] ?? 0); + +if ($config_enable_cron == 0) { + cronJobStop("Cron: is not enabled\n"); +} + +// An install whose files are newer than its schema reaches this job before the migration +// that adds the columns has run. Asking for them would throw +if (!settingsColumnExists($mysqli, 'config_update_latest_commit')) { + cronJobStop("The database update has not been applied yet\n"); +} + +// A zip-drop install has no remote to ask. Not a failure - there is simply nothing to check +if (!file_exists("../.git")) { + cronJobStop("Not a git checkout - there is no remote to check against\n"); +} + +if (!shellCommandsAvailable()) { + cronJobStop("PHP on the command line cannot run git (exec is disabled), so the check cannot run\n"); +} + +$updates = checkForUpdates(); + +/* + * Thrown rather than stopped: a fetch that fails is something to look at, and the dispatcher + * records a thrown message against the job so it shows on both Maintenance > Cron and the + * Update page. The last three lines, not the last one - git's final line is often the + * suggested fix rather than the problem. + */ +if ($updates->result !== 0) { + $update_check_error = trim(implode(' | ', array_slice($updates->output, -3))); + throw new Exception("git fetch failed: " . ($update_check_error === '' ? 'no output' : $update_check_error)); +} + +$update_check_latest = escapeSql($updates->latest_version); +$update_check_commits = escapeSql((string) json_encode($updates->pending_commits)); + +mysqli_query($mysqli, "UPDATE settings SET + config_update_latest_commit = '$update_check_latest', + config_update_pending_commits = '$update_check_commits', + config_update_checked_at = '" . date('Y-m-d H:i:s') . "' + WHERE company_id = 1"); + +echo count($updates->pending_commits) . " commit(s) behind the remote\n"; diff --git a/db.sql b/db.sql index 7ed5c6fe4..56df8241c 100644 --- a/db.sql +++ b/db.sql @@ -2349,6 +2349,9 @@ CREATE TABLE `settings` ( `config_backup_cron_type` varchar(20) NOT NULL DEFAULT 'full', `config_update_queued_at` datetime DEFAULT NULL, `config_internal_client_id` int(11) NOT NULL DEFAULT 0, + `config_update_latest_commit` varchar(40) DEFAULT NULL, + `config_update_pending_commits` text DEFAULT NULL, + `config_update_checked_at` datetime DEFAULT NULL, PRIMARY KEY (`company_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -3193,4 +3196,4 @@ CREATE TABLE `vendors` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2026-08-25 12:18:16 +-- Dump completed on 2026-08-26 23:37:25 diff --git a/functions/app.php b/functions/app.php index 851b1db96..884ed6676 100644 --- a/functions/app.php +++ b/functions/app.php @@ -336,11 +336,201 @@ function getRepoBranch(): string } /* - * Whether this PHP can run external commands at all. Applying an update is cron's job now, so - * the only thing the web tier still wants a shell for is READING git - checkForUpdates() and - * the pending-commit list on Maintenance > Update. Hosts that disable exec()/shell_exec() - - * shared hosting, a hardened php.ini, an FPM pool locked down while the CLI is not - lose the - * "an update is waiting" readout, not the ability to update. + * Reading git WITHOUT running git. + * + * Everything below answers from the files in .git, so it works on hosts where exec() and + * shell_exec() are disabled and costs a couple of file reads instead of a process. Only + * operations that need the network (git fetch) or the object database (a commit list) still + * need the binary - see CONTRIBUTING rule 6. + * + * Returns '' rather than throwing on anything unexpected: a zip-drop install has no .git at + * all, and every caller here is reporting, not deciding. + */ +function gitDir(): string +{ + $root = dirname(__DIR__); + $git = $root . '/.git'; + + if (is_dir($git)) { + return $git; + } + + // Submodules and linked worktrees put "gitdir: " in a file instead of a directory + if (is_file($git)) { + $line = trim((string) @file_get_contents($git)); + + if (str_starts_with($line, 'gitdir:')) { + $path = trim(substr($line, 7)); + + if ($path !== '' && $path[0] !== '/') { + $path = $root . '/' . $path; + } + + if ($path !== '' && is_dir($path)) { + return $path; + } + } + } + + return ''; +} + +/* + * Where the refs live. A linked worktree keeps its own HEAD but shares the main repository's + * refs, and points at them with a commondir file - resolve a ref against the worktree's own + * directory and every lookup comes back empty. + */ +function gitCommonDir(): string +{ + $dir = gitDir(); + + if ($dir === '') { + return ''; + } + + $commondir = $dir . '/commondir'; + + if (is_file($commondir)) { + $path = trim((string) @file_get_contents($commondir)); + + if ($path !== '' && $path[0] !== '/') { + $path = $dir . '/' . $path; + } + + if ($path !== '' && is_dir($path)) { + return rtrim($path, '/'); + } + } + + return $dir; +} + +/* + * The commit a ref points at, e.g. gitRefCommit('refs/remotes/origin/develop'). + * + * A ref is either its own file or a line in packed-refs; git writes loose files and moves + * them into packed-refs when it tidies up, so both have to be read. Lines starting with ^ in + * packed-refs are the peeled target of an annotated tag, not a ref. + */ +function gitRefCommit(string $ref, int $depth = 0): string +{ + // The ref becomes part of a path, and $repo_branch reaches this from config.php + if ($depth > 5 || str_contains($ref, '..') || !preg_match('#^[A-Za-z0-9._/-]+$#', $ref)) { + return ''; + } + + $dir = gitCommonDir(); + + if ($dir === '') { + return ''; + } + + $loose = $dir . '/' . $ref; + + if (is_file($loose)) { + $value = trim((string) @file_get_contents($loose)); + + if (str_starts_with($value, 'ref: ')) { + return gitRefCommit(trim(substr($value, 5)), $depth + 1); + } + + return preg_match('/^[0-9a-f]{40}$/', $value) ? $value : ''; + } + + $packed = $dir . '/packed-refs'; + + if (is_file($packed)) { + foreach ((array) @file($packed, FILE_IGNORE_NEW_LINES) as $line) { + + if ($line === '' || $line[0] === '#' || $line[0] === '^') { + continue; + } + + $parts = explode(' ', $line, 2); + + if (count($parts) === 2 && trim($parts[1]) === $ref) { + return preg_match('/^[0-9a-f]{40}$/', $parts[0]) ? $parts[0] : ''; + } + + } + } + + return ''; +} + +/* The branch this working tree is on, or 'HEAD' when it is detached. */ +function gitCurrentBranch(): string +{ + $dir = gitDir(); + + if ($dir === '') { + return ''; + } + + $head = trim((string) @file_get_contents($dir . '/HEAD')); + + if (str_starts_with($head, 'ref: refs/heads/')) { + return substr($head, 16); + } + + return $head === '' ? '' : 'HEAD'; +} + +/* The commit this working tree is on - the file-read equivalent of git rev-parse HEAD. */ +function gitCurrentCommit(): string +{ + $dir = gitDir(); + + if ($dir === '') { + return ''; + } + + $head = trim((string) @file_get_contents($dir . '/HEAD')); + + if (str_starts_with($head, 'ref: ')) { + return gitRefCommit(trim(substr($head, 5))); + } + + return preg_match('/^[0-9a-f]{40}$/', $head) ? $head : ''; +} + +/* + * Where a command lives, or '' if it is not on the path - what `which` was being run for. + * + * PHP-FPM pools often ship a nearly empty PATH, so the usual locations are checked as well; + * a missing hit here would otherwise read as "git is not installed" on a box where it is. + */ +function commandPath(string $command): string +{ + if (!preg_match('/^[A-Za-z0-9._-]+$/', $command)) { + return ''; + } + + $dirs = array_filter(explode(PATH_SEPARATOR, (string) getenv('PATH'))); + + foreach (['/usr/local/sbin', '/usr/local/bin', '/usr/sbin', '/usr/bin', '/sbin', '/bin'] as $fallback) { + if (!in_array($fallback, $dirs, true)) { + $dirs[] = $fallback; + } + } + + foreach ($dirs as $dir) { + $candidate = rtrim($dir, '/') . '/' . $command; + + if (@is_file($candidate) && @is_executable($candidate)) { + return $candidate; + } + } + + return ''; +} + +/* + * Whether this PHP can run external commands at all. Nothing in the web tier does any more - + * checkForUpdates() is called from cron/update_check.php alone, and the Update page reads what + * that job stored. This is what the job asks before it starts, and what admin/debug.php + * reports, so a host with exec()/shell_exec() disabled shows why its checks stopped rather + * than failing silently. * * function_exists() already reports a disabled function as missing. disable_functions is * read as well because some hardening extensions leave the function defined and refuse the @@ -393,18 +583,47 @@ function checkForUpdates() { $updates->current_version = ''; $updates->latest_version = ''; $updates->update_message = "Cannot check for updates"; + $updates->pending_commits = []; return $updates; } - $remote_ref = escapeshellarg("origin/" . getRepoBranch()); - // Fetch the latest code changes but don't apply them. stderr is merged in because git // reports failures there, and it is the only thing the update page can show when this // breaks - it used to run a second git fetch of its own just to get the message. exec("git fetch 2>&1", $output, $result); - $latest_version = exec("git rev-parse $remote_ref"); - $current_version = exec("git rev-parse HEAD"); + + // Both sides of the comparison are read out of .git rather than shelled for - the fetch + // has already written the remote-tracking ref by the time we get here + $latest_version = gitRefCommit("refs/remotes/origin/" . getRepoBranch()); + $current_version = gitCurrentCommit(); + + /* + * The commits between here and there. Fields are separated by \x1f rather than letting + * git format the row itself, because a subject comes from outside this install and used + * to reach the Update page as unescaped HTML. + * + * The date is %aI (absolute, ISO 8601) rather than %ar. This result is stored and read + * back hours later, and a stored "2 hours ago" is wrong the moment it is written - the + * relative form is worked out at render time instead. + */ + $updates->pending_commits = []; + + $remote_ref = escapeshellarg("origin/" . getRepoBranch()); + + foreach (explode("\n", trim((string) shell_exec("git log HEAD..$remote_ref --pretty=format:'%h%x1f%aI%x1f%s'"))) as $commit_line) { + + if ($commit_line === '') { + continue; + } + + $commit_fields = explode("\x1f", $commit_line, 3); + + if (count($commit_fields) === 3) { + $updates->pending_commits[] = $commit_fields; + } + + } if ($current_version == $latest_version) { $update_message = "No Updates available"; diff --git a/includes/cron_jobs.php b/includes/cron_jobs.php index 125ce152a..f3504c86e 100644 --- a/includes/cron_jobs.php +++ b/includes/cron_jobs.php @@ -102,6 +102,14 @@ function cronJobRegistry(): array 'schedule' => 'Daily', 'daily_at' => '03:30', ], + [ + 'name' => 'update_check', + 'label' => 'Update Check', + 'script' => 'update_check.php', + 'description' => 'Asks the git remote whether a newer release exists and stores the answer for Maintenance > Update.', + 'schedule' => 'Daily', + 'daily_at' => '02:30', + ], [ 'name' => 'app_update', 'label' => 'Application Update', diff --git a/js/auto_reload.js b/js/auto_reload.js new file mode 100644 index 000000000..277e80c2d --- /dev/null +++ b/js/auto_reload.js @@ -0,0 +1,34 @@ +/** + * Reload a page that is waiting on something cron will do. + * + * Opt in by putting data-itflow-reload-seconds="" on the element that says so - the + * "Checking for updates" alert on Maintenance > Update is the first user. Rendering that + * attribute is what starts the timer, so a view that is not waiting costs one failed + * querySelector. + * + * Deliberately an attribute rather than an inline