mirror of
https://github.com/itflow-org/itflow
synced 2026-09-22 06:31:15 +00:00
Implemented Check for updates which removs the last shell exec from web ui, and reimplemented db_update switch to update_cli.php
This commit is contained in:
26
admin/database_updates/2.7.3.php
Normal file
26
admin/database_updates/2.7.3.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ITFlow - Database update to version 2.7.3 (from 2.7.2)
|
||||||
|
* Included by admin/database_updates.php - do not access directly
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||||
|
|
||||||
|
// Maintenance > 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'");
|
||||||
@@ -6,11 +6,10 @@ require_once "../config.php";
|
|||||||
|
|
||||||
$checks = [];
|
$checks = [];
|
||||||
|
|
||||||
// Execute the git command to get the latest commit hash
|
// Read straight out of .git - no shell needed, and it still answers on a hardened host
|
||||||
$commitHash = shell_exec('git log -1 --format=%H');
|
$commitHash = gitCurrentCommit();
|
||||||
|
|
||||||
// Get branch info
|
$gitBranch = gitCurrentBranch();
|
||||||
$gitBranch = shell_exec('git rev-parse --abbrev-ref HEAD');
|
|
||||||
|
|
||||||
// Section: System Information
|
// Section: System Information
|
||||||
$systemInfo = [];
|
$systemInfo = [];
|
||||||
@@ -146,27 +145,15 @@ $phpConfig[] = [
|
|||||||
// Section: Shell Commands
|
// Section: Shell Commands
|
||||||
$shellCommands = [];
|
$shellCommands = [];
|
||||||
|
|
||||||
if ($shell_exec_enabled) {
|
// Located by walking PATH rather than by running `which`, so this reports the truth on a
|
||||||
$commands = ['git'];
|
// host with shell_exec disabled instead of reporting the host's php.ini back at itself
|
||||||
|
foreach (['git'] as $command) {
|
||||||
foreach ($commands as $command) {
|
$path = commandPath($command);
|
||||||
$which = trim(shell_exec("which $command 2>/dev/null"));
|
|
||||||
$exists = !empty($which);
|
|
||||||
$shellCommands[] = [
|
$shellCommands[] = [
|
||||||
'name' => "Command '$command' available",
|
'name' => "Command '$command' available",
|
||||||
'passed' => $exists,
|
'passed' => $path !== '',
|
||||||
'value' => $exists ? $which : 'Not Found',
|
'value' => $path !== '' ? $path : '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',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section: SSL Checks
|
// Section: SSL Checks
|
||||||
@@ -531,11 +518,11 @@ $mysqli->close();
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Current Code Commit</td>
|
<td>Current Code Commit</td>
|
||||||
<td><?= $commitHash ?></td>
|
<td><?= $commitHash === '' ? 'Not a git checkout' : escapeHtml($commitHash) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Current Branch</td>
|
<td>Current Branch</td>
|
||||||
<td><?= $gitBranch ?></td>
|
<td><?= $gitBranch === '' ? 'Not a git checkout' : escapeHtml($gitBranch) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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
|
* 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.
|
* 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'])) {
|
if (isset($_GET['queue_update'])) {
|
||||||
|
|
||||||
validateCSRFToken();
|
validateCSRFToken();
|
||||||
|
|||||||
142
admin/update.php
142
admin/update.php
@@ -4,52 +4,51 @@ require_once "includes/inc_all_admin.php";
|
|||||||
require_once "../includes/database_version.php";
|
require_once "../includes/database_version.php";
|
||||||
|
|
||||||
$repo_branch = getRepoBranch();
|
$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
|
* This page does not run git. cron/update_check.php does the fetch on its own schedule and
|
||||||
* shell gate below therefore only decides whether this page can READ the git remote to say
|
* stores what it found; everything below is a read of that, plus .git for the local commit.
|
||||||
* an update is waiting; where PHP cannot run one - shared hosting, a hardened php.ini, an
|
* Check Now asks the dispatcher for a run rather than checking inside the request, so the
|
||||||
* FPM pool locked down while the command line is not - the page offers the queue blind,
|
* page works the same on a host whose web PHP cannot run external commands at all.
|
||||||
* which is harmless when there is nothing to fetch. The database half is plain PHP and
|
|
||||||
* works everywhere.
|
|
||||||
*/
|
*/
|
||||||
$shell_available = shellCommandsAvailable();
|
$current_version = gitCurrentCommit();
|
||||||
|
|
||||||
$current_version = '';
|
// The stored answer arrives with a migration, and this page has to render on an install
|
||||||
$fetch_ok = true;
|
// whose files are newer than its schema
|
||||||
$updates = null;
|
$update_check_available = settingsColumnExists($mysqli, 'config_update_latest_commit');
|
||||||
|
|
||||||
// Commits sitting between this working tree and the remote branch. Fields are separated
|
$latest_version = '';
|
||||||
// by \x1f rather than having git build the table markup, because a commit subject comes
|
$update_checked_at = null;
|
||||||
// from outside this install and used to reach the page as unescaped HTML.
|
|
||||||
$pending_commits = [];
|
$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;
|
$latest_version = (string) ($update_check_row['config_update_latest_commit'] ?? '');
|
||||||
$fetch_ok = $updates->result === 0;
|
$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 (is_array($stored_commits)) {
|
||||||
|
foreach ($stored_commits as $stored_commit) {
|
||||||
if ($commit_line === '') {
|
if (is_array($stored_commit) && count($stored_commit) === 3) {
|
||||||
continue;
|
$pending_commits[] = array_values($stored_commit);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$commit_fields = explode("\x1f", $commit_line, 3);
|
$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"));
|
||||||
|
|
||||||
if (count($commit_fields) === 3) {
|
|
||||||
$pending_commits[] = $commit_fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
* 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
|
* 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
|
// 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.
|
// the plain comparison silently stops offering database updates once a minor reaches 10.
|
||||||
$db_update_available = version_compare(LATEST_DATABASE_VERSION, CURRENT_DATABASE_VERSION, '>');
|
$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);
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<?php if ($shell_available && !$fetch_ok) { ?>
|
<?php if (!empty($check_job['cron_job_last_error'])) { ?>
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<h5><i class="fas fa-fw fa-exclamation-triangle me-2"></i>Cannot reach the Git remote</h5>
|
<h5><i class="fas fa-fw fa-exclamation-triangle me-2"></i>The last update check did not finish</h5>
|
||||||
ITFlow updates itself with Git, so nothing below is current until this is fixed.
|
Anything below is from the check before it, so it may be out of date.
|
||||||
<?php if (!empty($updates->output)) { ?>
|
<pre class="bg-dark text-white p-2 mt-2 mb-2"><?= escapeHtml($check_job['cron_job_last_error']) ?></pre>
|
||||||
<pre class="bg-dark text-white p-2 mt-2 mb-2"><?= escapeHtml(implode("\n", $updates->output)) ?></pre>
|
Recorded <?= escapeHtml((string) $check_job['cron_job_last_error_at']) ?>. Check that Git is installed
|
||||||
<?php } ?>
|
and that the remote is reachable from this server, then clear the error from
|
||||||
Check that Git is installed, that the remote is reachable from this server, and that the web server
|
<a href="cron.php" class="alert-link">Maintenance > Cron</a>. The
|
||||||
user can write to the ITFlow directory. The
|
<a href="https://forum.itflow.org" class="alert-link" target="_blank">forum</a> can help - include the
|
||||||
<a href="https://forum.itflow.org" class="alert-link" target="_blank">forum</a> can help - include
|
output above.
|
||||||
your PHP error log and the output above.
|
|
||||||
</div>
|
</div>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<?php if (!$shell_available) { ?>
|
<?php if ($check_in_progress) { ?>
|
||||||
<div class="alert alert-info">
|
<?php /* data-itflow-reload-seconds is read by js/auto_reload.js, loaded at the foot
|
||||||
<h5><i class="fas fa-fw fa-info-circle me-2"></i>This server cannot check for updates</h5>
|
of this page. The dispatcher clears run_now when it claims the job, so one
|
||||||
PHP here has <code>exec</code> and <code>shell_exec</code> disabled, so this page cannot read the Git
|
reload a few seconds later is normally enough to land on the result. */ ?>
|
||||||
remote to tell you whether one is waiting. Queueing still works - cron runs the update from the command
|
<div class="alert alert-info" data-itflow-reload-seconds="15">
|
||||||
line, which is usually not restricted the same way. Database updates are plain PHP and are unaffected.
|
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||||
|
<strong>Checking for updates.</strong>
|
||||||
|
<?php if ($cron_is_running) { ?>
|
||||||
|
Cron runs the check within a minute; this page refreshes itself.
|
||||||
|
<?php } else { ?>
|
||||||
|
<strong class="text-warning">Nothing is going to pick it up</strong> - cron has not checked in
|
||||||
|
recently or the master switch is off. See
|
||||||
|
<a href="cron.php" class="alert-link">Maintenance > Cron</a>.
|
||||||
|
<?php } ?>
|
||||||
</div>
|
</div>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
@@ -144,6 +155,25 @@ $app_update_available = !empty($pending_commits);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php /* Outside the pending block on purpose - re-checking has to be reachable from the
|
||||||
|
up-to-date view too, which is where somebody goes when they have just read that
|
||||||
|
a release is out. */ ?>
|
||||||
|
<p class="text-muted mb-3">
|
||||||
|
<small>
|
||||||
|
<?php if (!empty($update_checked_at)) { ?>
|
||||||
|
Last checked <?= escapeHtml(timeAgo($update_checked_at)) ?>
|
||||||
|
(<?= escapeHtml($update_checked_at) ?>).
|
||||||
|
<?php } else { ?>
|
||||||
|
Never checked.
|
||||||
|
<?php } ?>
|
||||||
|
<?php if ($update_check_available && !$check_in_progress) { ?>
|
||||||
|
<a href="post.php?check_update&csrf_token=<?= $_SESSION['csrf_token'] ?>">Check now</a>
|
||||||
|
<?php } elseif ($check_in_progress) { ?>
|
||||||
|
<span class="text-secondary">Checking…</span>
|
||||||
|
<?php } ?>
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
|
||||||
<?php if (!empty($update_queued_at)) { ?>
|
<?php if (!empty($update_queued_at)) { ?>
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<i class="fas fa-fw fa-clock me-2"></i>An update was queued at
|
<i class="fas fa-fw fa-clock me-2"></i>An update was queued at
|
||||||
@@ -168,7 +198,7 @@ $app_update_available = !empty($pending_commits);
|
|||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<?php if ($shell_available && !$app_update_available && !$db_update_available) { ?>
|
<?php if (!empty($update_checked_at) && !$app_update_available && !$db_update_available) { ?>
|
||||||
|
|
||||||
<div class="text-center py-3">
|
<div class="text-center py-3">
|
||||||
<i class="far fa-3x fa-smile-wink text-dark"></i>
|
<i class="far fa-3x fa-smile-wink text-dark"></i>
|
||||||
@@ -198,7 +228,7 @@ $app_update_available = !empty($pending_commits);
|
|||||||
together under one button rather than as separate steps. The db_update_available
|
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
|
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. */ ?>
|
exactly the state that needs queueing, and without it the button would not draw. */ ?>
|
||||||
<?php if ($app_update_available || $db_update_available || !$shell_available) { ?>
|
<?php if ($app_update_available || $db_update_available || empty($update_checked_at)) { ?>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h6 class="text-uppercase text-secondary">Pending</h6>
|
<h6 class="text-uppercase text-secondary">Pending</h6>
|
||||||
|
|
||||||
@@ -208,11 +238,11 @@ $app_update_available = !empty($pending_commits);
|
|||||||
<?= count($pending_commits) ?> commit<?= count($pending_commits) === 1 ? '' : 's' ?>
|
<?= count($pending_commits) ?> commit<?= count($pending_commits) === 1 ? '' : 's' ?>
|
||||||
behind <code><?= escapeHtml("origin/$repo_branch") ?></code>.
|
behind <code><?= escapeHtml("origin/$repo_branch") ?></code>.
|
||||||
</p>
|
</p>
|
||||||
<?php } elseif (!$shell_available) { ?>
|
<?php } elseif (empty($update_checked_at)) { ?>
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
<strong>Application files:</strong> this server cannot check
|
<strong>Application files:</strong> this install has not checked
|
||||||
<code><?= escapeHtml("origin/$repo_branch") ?></code>, so there may or may not be anything
|
<code><?= escapeHtml("origin/$repo_branch") ?></code> yet, so there may or may not be
|
||||||
waiting. Queueing an update when there is nothing to do is harmless.
|
anything waiting. Queueing an update when there is nothing to do is harmless.
|
||||||
</p>
|
</p>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
@@ -272,7 +302,9 @@ $app_update_available = !empty($pending_commits);
|
|||||||
<?php foreach ($pending_commits as $commit) { ?>
|
<?php foreach ($pending_commits as $commit) { ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code><?= escapeHtml($commit[0]) ?></code></td>
|
<td><code><?= escapeHtml($commit[0]) ?></code></td>
|
||||||
<td class="text-nowrap"><?= escapeHtml($commit[1]) ?></td>
|
<?php /* stored as an absolute ISO date by the check job - a stored
|
||||||
|
"2 hours ago" would be wrong the moment it was written */ ?>
|
||||||
|
<td class="text-nowrap"><?= escapeHtml(timeAgo($commit[1])) ?></td>
|
||||||
<td><?= escapeHtml($commit[2]) ?></td>
|
<td><?= escapeHtml($commit[2]) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
@@ -284,6 +316,8 @@ $app_update_available = !empty($pending_commits);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="../js/auto_reload.js"></script>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
require_once "../includes/footer.php";
|
require_once "../includes/footer.php";
|
||||||
|
|||||||
@@ -1156,7 +1156,7 @@ while ($row = mysqli_fetch_assoc($sql_invalid_recurring_expenses)) {
|
|||||||
|
|
||||||
if ($config_telemetry > 0 || $config_telemetry == 2) {
|
if ($config_telemetry > 0 || $config_telemetry == 2) {
|
||||||
|
|
||||||
$current_version = exec("git rev-parse HEAD");
|
$current_version = gitCurrentCommit();
|
||||||
|
|
||||||
// Client Count
|
// Client Count
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('client_id') AS num FROM clients"));
|
$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
|
// 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"));
|
||||||
|
|
||||||
if ($updates->current_version !== $updates->latest_version) {
|
$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
|
// Send Alert to inform Updates Available
|
||||||
appNotify("Update", "$update_message", "/admin/update.php");
|
appNotify("Update", "New Updates are Available [$latest_version]", "/admin/update.php");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
81
cron/update_check.php
Normal file
81
cron/update_check.php
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ITFlow - Update check job
|
||||||
|
*
|
||||||
|
* Asks the git remote whether a newer release exists and stores the answer. Nothing in the
|
||||||
|
* web tier shells out any more, so this is the only place the question gets asked: the
|
||||||
|
* Update page and the nightly notification both read what this job wrote.
|
||||||
|
*
|
||||||
|
* Check Now on Maintenance > 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";
|
||||||
5
db.sql
5
db.sql
@@ -2349,6 +2349,9 @@ CREATE TABLE `settings` (
|
|||||||
`config_backup_cron_type` varchar(20) NOT NULL DEFAULT 'full',
|
`config_backup_cron_type` varchar(20) NOT NULL DEFAULT 'full',
|
||||||
`config_update_queued_at` datetime DEFAULT NULL,
|
`config_update_queued_at` datetime DEFAULT NULL,
|
||||||
`config_internal_client_id` int(11) NOT NULL DEFAULT 0,
|
`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`)
|
PRIMARY KEY (`company_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
@@ -3193,4 +3196,4 @@ CREATE TABLE `vendors` (
|
|||||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
/*!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
|
||||||
|
|||||||
@@ -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
|
* Reading git WITHOUT running git.
|
||||||
* 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() -
|
* Everything below answers from the files in .git, so it works on hosts where exec() and
|
||||||
* shared hosting, a hardened php.ini, an FPM pool locked down while the CLI is not - lose the
|
* shell_exec() are disabled and costs a couple of file reads instead of a process. Only
|
||||||
* "an update is waiting" readout, not the ability to update.
|
* 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: <path>" 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
|
* 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
|
* read as well because some hardening extensions leave the function defined and refuse the
|
||||||
@@ -393,18 +583,47 @@ function checkForUpdates() {
|
|||||||
$updates->current_version = '';
|
$updates->current_version = '';
|
||||||
$updates->latest_version = '';
|
$updates->latest_version = '';
|
||||||
$updates->update_message = "Cannot check for updates";
|
$updates->update_message = "Cannot check for updates";
|
||||||
|
$updates->pending_commits = [];
|
||||||
|
|
||||||
return $updates;
|
return $updates;
|
||||||
}
|
}
|
||||||
|
|
||||||
$remote_ref = escapeshellarg("origin/" . getRepoBranch());
|
|
||||||
|
|
||||||
// Fetch the latest code changes but don't apply them. stderr is merged in because git
|
// 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
|
// 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.
|
// breaks - it used to run a second git fetch of its own just to get the message.
|
||||||
exec("git fetch 2>&1", $output, $result);
|
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) {
|
if ($current_version == $latest_version) {
|
||||||
$update_message = "No Updates available";
|
$update_message = "No Updates available";
|
||||||
|
|||||||
@@ -102,6 +102,14 @@ function cronJobRegistry(): array
|
|||||||
'schedule' => 'Daily',
|
'schedule' => 'Daily',
|
||||||
'daily_at' => '03:30',
|
'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',
|
'name' => 'app_update',
|
||||||
'label' => 'Application Update',
|
'label' => 'Application Update',
|
||||||
|
|||||||
34
js/auto_reload.js
Normal file
34
js/auto_reload.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Reload a page that is waiting on something cron will do.
|
||||||
|
*
|
||||||
|
* Opt in by putting data-itflow-reload-seconds="<n>" 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 <script>: inline scripts are the thing
|
||||||
|
* standing between ITFlow and a CSP without unsafe-inline, and this would have been another.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
|
const waiting = document.querySelector('[data-itflow-reload-seconds]');
|
||||||
|
|
||||||
|
if (!waiting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seconds = parseInt(waiting.dataset.itflowReloadSeconds, 10);
|
||||||
|
|
||||||
|
if (!Number.isFinite(seconds) || seconds < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
window.location.reload();
|
||||||
|
}, seconds * 1000);
|
||||||
|
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -130,7 +130,7 @@ echo "\nDone.\n";
|
|||||||
echo "\nNext steps:\n";
|
echo "\nNext steps:\n";
|
||||||
echo " 1. Log in with the credentials that were in use when the backup was taken.\n";
|
echo " 1. Log in with the credentials that were in use when the backup was taken.\n";
|
||||||
echo " 2. If the backup is older than the code in this directory, finish the database update:\n";
|
echo " 2. If the backup is older than the code in this directory, finish the database update:\n";
|
||||||
echo " php " . realpath(__DIR__) . "/update_cli.php\n";
|
echo " php " . realpath(__DIR__) . "/update_cli.php --update_db\n";
|
||||||
echo " 3. Check Maintenance > Cron - the schedule came back with the database.\n";
|
echo " 3. Check Maintenance > Cron - the schedule came back with the database.\n";
|
||||||
|
|
||||||
exit(0);
|
exit(0);
|
||||||
|
|||||||
@@ -24,19 +24,25 @@ require_once "../config.php";
|
|||||||
require_once "../functions.php";
|
require_once "../functions.php";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This script takes no options. It updates the application and then the database, in that
|
* Run with no options this updates the application and then the database, in that order,
|
||||||
* order, because new code against an old schema is what breaks an install mid-upgrade.
|
* because new code against an old schema is what breaks an install mid-upgrade.
|
||||||
*
|
*
|
||||||
* The application update is a hard reset onto the branch this install tracks, so local
|
* The application update is a hard reset onto the branch this install tracks, so local
|
||||||
* modifications to tracked files are discarded. Untracked files are left alone, which is
|
* modifications to tracked files are discarded. Untracked files are left alone, which is
|
||||||
* everything an install keeps for itself: config.php, uploads/ and the custom/ directories.
|
* everything an install keeps for itself: config.php, uploads/ and the custom/ directories.
|
||||||
|
*
|
||||||
|
* --update_db does the database half ON ITS OWN and never touches git. That matters on a box
|
||||||
|
* carrying work that is not pushed: the full run would hard-reset it away, and the database
|
||||||
|
* half is usually all that is wanted after a manual checkout or a restore.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function printUsage($stream = STDOUT) {
|
function printUsage($stream = STDOUT) {
|
||||||
fwrite($stream, "Usage: php update_cli.php\n\n");
|
fwrite($stream, "Usage: php update_cli.php [--update_db]\n\n");
|
||||||
fwrite($stream, "Updates the application to the latest code on the branch this install tracks,\n");
|
fwrite($stream, "With no options, updates the application to the latest code on the branch this\n");
|
||||||
fwrite($stream, "discarding local changes to tracked files, and then applies any outstanding\n");
|
fwrite($stream, "install tracks, discarding local changes to tracked files, and then applies any\n");
|
||||||
fwrite($stream, "database updates. There are no options.\n");
|
fwrite($stream, "outstanding database updates.\n\n");
|
||||||
|
fwrite($stream, " --update_db Apply outstanding database updates only. The application files\n");
|
||||||
|
fwrite($stream, " are left exactly as they are - git is not run at all.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -47,22 +53,39 @@ function printUsage($stream = STDOUT) {
|
|||||||
$database_phase_only = getenv('ITFLOW_UPDATE_PHASE') === 'database';
|
$database_phase_only = getenv('ITFLOW_UPDATE_PHASE') === 'database';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The switches this script used to take are gone. Anything on the command line is refused
|
* --db_update is accepted alongside --update_db on purpose. The two spellings have been used
|
||||||
* rather than ignored, so that an old --update_db call from a script or a set of notes
|
* interchangeably in this project's own notes and release steps for years, and the cost of a
|
||||||
* cannot silently trigger a hard reset of the application instead.
|
* typo here is the opposite of what was asked for - a hard reset instead of a database update.
|
||||||
|
* Anything else on the command line is still REFUSED rather than ignored, for the same reason.
|
||||||
*/
|
*/
|
||||||
|
$database_only_requested = false;
|
||||||
|
|
||||||
$arguments = array_slice($argv, 1);
|
$arguments = array_slice($argv, 1);
|
||||||
|
|
||||||
if (!$database_phase_only && count($arguments) > 0) {
|
if (!$database_phase_only && count($arguments) > 0) {
|
||||||
|
|
||||||
if (in_array($arguments[0], ['--help', '-h', 'help'], true)) {
|
if (count($arguments) === 1 && in_array($arguments[0], ['--help', '-h', 'help'], true)) {
|
||||||
printUsage();
|
printUsage();
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
fwrite(STDERR, "Error: this script takes no options.\n\n");
|
if (count($arguments) === 1 && in_array($arguments[0], ['--update_db', '--db_update', '--database'], true)) {
|
||||||
|
|
||||||
|
$database_phase_only = true;
|
||||||
|
$database_only_requested = true;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
fwrite(STDERR, "Error: unrecognised option.\n\n");
|
||||||
printUsage(STDERR);
|
printUsage(STDERR);
|
||||||
exit(1);
|
exit(1);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($database_only_requested) {
|
||||||
|
echo "Database updates only - the application files will not be touched.\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether the working tree actually moved. Decides how the database phase runs below
|
// Whether the working tree actually moved. Decides how the database phase runs below
|
||||||
|
|||||||
@@ -754,28 +754,18 @@ if (isset($_POST['add_telemetry'])) {
|
|||||||
// Section: Shell Commands
|
// Section: Shell Commands
|
||||||
$shellCommands = [];
|
$shellCommands = [];
|
||||||
|
|
||||||
if ($shell_exec_enabled) {
|
// Located by walking PATH rather than by running `which`, so this reports
|
||||||
$commands = ['git'];
|
// the truth on a host with shell_exec disabled. The no-shell branch this
|
||||||
|
// replaces also still listed whois and dig, which ITFlow stopped shelling
|
||||||
foreach ($commands as $command) {
|
// out to when domain lookups moved to RDAP and native DNS
|
||||||
$which = trim(shell_exec("which $command 2>/dev/null"));
|
foreach (['git'] as $command) {
|
||||||
$exists = !empty($which);
|
$path = commandPath($command);
|
||||||
$shellCommands[] = [
|
$shellCommands[] = [
|
||||||
'name' => "Command '$command' available",
|
'name' => "Command '$command' available",
|
||||||
'passed' => $exists,
|
'passed' => $path !== '',
|
||||||
'value' => $exists ? $which : 'Not Found',
|
'value' => $path !== '' ? $path : 'Not Found',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// If shell_exec is disabled, mark commands as unavailable
|
|
||||||
foreach (['whois', 'dig', 'git'] as $command) {
|
|
||||||
$shellCommands[] = [
|
|
||||||
'name' => "Command '$command' available",
|
|
||||||
'passed' => false,
|
|
||||||
'value' => 'shell_exec Disabled',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Section: SSL Checks
|
// Section: SSL Checks
|
||||||
$sslChecks = [];
|
$sslChecks = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user