mirror of
https://github.com/itflow-org/itflow
synced 2026-09-07 23:35:13 +00:00
Feature: queued Updates which now eliminates the need for shell_exec from the webui, but if enabled you can still update from the webui the old way too
This commit is contained in:
20
admin/database_updates/2.6.8.php
Normal file
20
admin/database_updates/2.6.8.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.6.8 (from 2.6.7)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Maintenance > Update can now hand an update to cron rather than running it inside the
|
||||
// request: the page stamps this column and cron/app_update.php takes it and runs
|
||||
// scripts/update_cli.php in its own process. NULL means nothing is queued, which is why
|
||||
// the job cannot update an install that did not ask for one.
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE `settings` ADD COLUMN IF NOT EXISTS `config_update_queued_at` datetime DEFAULT NULL");
|
||||
|
||||
// Seeded here as well as by the dispatcher, which only creates rows on its next pass -
|
||||
// without this, an update queued 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 = 'app_update', cron_job_enabled = 0, cron_job_schedule = 'Daily', cron_job_daily_at = '05:00'");
|
||||
@@ -2,12 +2,53 @@
|
||||
|
||||
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
||||
|
||||
/*
|
||||
* Hands the update to cron instead of running it in this request. scripts/update_cli.php
|
||||
* replaces the files this request is executing from and then applies the migrations that
|
||||
* came with them, which is not something to do half way through a page load - and on a host
|
||||
* where PHP cannot run external commands it is the only way to update the application at all.
|
||||
*
|
||||
* The row is written as well as the settings column: config_update_queued_at is what
|
||||
* 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.
|
||||
*/
|
||||
if (isset($_GET['queue_update'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceAdminPermission();
|
||||
|
||||
// The files can be newer than the schema - that is the window this whole page exists to
|
||||
// close - and the column the queue is written to arrives with a migration
|
||||
if (!settingsColumnExists($mysqli, 'config_update_queued_at')) {
|
||||
flashAlert("Apply the database update first - queueing needs a schema change this install has not caught up with yet.", 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "UPDATE settings SET config_update_queued_at = '" . date('Y-m-d H:i:s') . "' WHERE company_id = 1");
|
||||
mysqli_query($mysqli, "UPDATE cron_jobs SET cron_job_run_now = 1 WHERE cron_job_name = 'app_update'");
|
||||
|
||||
logAudit("App", "Update", "$session_name queued an update to be applied by cron");
|
||||
|
||||
flashAlert("Update queued - cron will start it within a minute.");
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_GET['update'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceAdminPermission();
|
||||
|
||||
// Reached only from buttons this install cannot draw without a shell, so anything
|
||||
// arriving here without one is a crafted request rather than somebody's mistake
|
||||
if (!shellCommandsAvailable()) {
|
||||
flashAlert("PHP on this server cannot run Git. Queue the update instead and cron will apply it.", 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
// git fetch downloads the latest from the remote without merging or rebasing anything.
|
||||
// The hard reset then throws away every local change and makes the working tree match
|
||||
// the tracked branch exactly.
|
||||
|
||||
166
admin/update.php
166
admin/update.php
@@ -6,32 +6,80 @@ require_once "../includes/database_version.php";
|
||||
$repo_branch = getRepoBranch();
|
||||
$remote_ref = escapeshellarg("origin/$repo_branch");
|
||||
|
||||
$updates = checkForUpdates();
|
||||
/*
|
||||
* Everything git can tell us needs a shell. Where PHP cannot run one - shared hosting, a
|
||||
* hardened php.ini, an FPM pool locked down while the command line is not - the page cannot
|
||||
* say whether an update is waiting and the web server cannot apply one either, so the
|
||||
* application half of this page is replaced by the queue, which cron carries out. The
|
||||
* database half is plain PHP and works everywhere.
|
||||
*/
|
||||
$shell_available = shellCommandsAvailable();
|
||||
|
||||
$current_version = $updates->current_version;
|
||||
$fetch_ok = $updates->result === 0;
|
||||
$current_version = '';
|
||||
$fetch_ok = true;
|
||||
$updates = null;
|
||||
|
||||
// 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.
|
||||
$pending_commits = [];
|
||||
|
||||
$git_log = shell_exec("git log HEAD..$remote_ref --pretty=format:'%h%x1f%ar%x1f%s'");
|
||||
if ($shell_available) {
|
||||
|
||||
foreach (explode("\n", trim((string) $git_log)) as $commit_line) {
|
||||
$updates = checkForUpdates();
|
||||
|
||||
if ($commit_line === '') {
|
||||
continue;
|
||||
}
|
||||
$current_version = $updates->current_version;
|
||||
$fetch_ok = $updates->result === 0;
|
||||
|
||||
$commit_fields = explode("\x1f", $commit_line, 3);
|
||||
$git_log = shell_exec("git log HEAD..$remote_ref --pretty=format:'%h%x1f%ar%x1f%s'");
|
||||
|
||||
foreach (explode("\n", trim((string) $git_log)) as $commit_line) {
|
||||
|
||||
if ($commit_line === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$commit_fields = explode("\x1f", $commit_line, 3);
|
||||
|
||||
if (count($commit_fields) === 3) {
|
||||
$pending_commits[] = $commit_fields;
|
||||
}
|
||||
|
||||
if (count($commit_fields) === 3) {
|
||||
$pending_commits[] = $commit_fields;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* 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
|
||||
* its own process. Neither column is in the global settings load - one page needs them.
|
||||
*
|
||||
* The column is checked for rather than assumed. This page has to render on an install whose
|
||||
* files are newer than its schema, because that is the state it exists to get people out of.
|
||||
*/
|
||||
$update_queue_available = settingsColumnExists($mysqli, 'config_update_queued_at');
|
||||
|
||||
$update_queued_at = null;
|
||||
$cron_last_dispatch_at = null;
|
||||
$update_job = null;
|
||||
|
||||
if ($update_queue_available) {
|
||||
|
||||
$update_cron_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_update_queued_at, config_cron_last_dispatch_at FROM settings WHERE company_id = 1"));
|
||||
|
||||
$update_queued_at = $update_cron_row['config_update_queued_at'] ?? null;
|
||||
$cron_last_dispatch_at = $update_cron_row['config_cron_last_dispatch_at'] ?? null;
|
||||
|
||||
$update_job = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT cron_job_last_run_at, cron_job_last_status, cron_job_last_error, cron_job_last_error_at FROM cron_jobs WHERE cron_job_name = 'app_update' LIMIT 1"));
|
||||
|
||||
}
|
||||
|
||||
// Queueing is only worth offering if something is going to pick the request up
|
||||
$cron_is_running = $update_queue_available
|
||||
&& !empty($config_enable_cron)
|
||||
&& !empty($cron_last_dispatch_at)
|
||||
&& strtotime($cron_last_dispatch_at) > strtotime('-5 minutes');
|
||||
|
||||
// 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, '>');
|
||||
@@ -45,7 +93,7 @@ $app_update_available = !empty($pending_commits);
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<?php if (!$fetch_ok) { ?>
|
||||
<?php if ($shell_available && !$fetch_ok) { ?>
|
||||
<div class="alert alert-danger">
|
||||
<h5><i class="fas fa-fw fa-exclamation-triangle me-2"></i>Cannot reach the Git remote</h5>
|
||||
ITFlow updates itself with Git, so nothing below is current until this is fixed.
|
||||
@@ -59,6 +107,15 @@ $app_update_available = !empty($pending_commits);
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<?php if (!$shell_available) { ?>
|
||||
<div class="alert alert-info">
|
||||
<h5><i class="fas fa-fw fa-info-circle me-2"></i>This server cannot run Git from the web</h5>
|
||||
PHP here has <code>exec</code> and <code>shell_exec</code> disabled, so this page can neither check for
|
||||
application updates nor apply one. Queue an update instead: cron runs it from the command line, which
|
||||
is usually not restricted the same way. Database updates are plain PHP and are unaffected.
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-6 mb-3">
|
||||
<small class="text-secondary text-uppercase">Release</small>
|
||||
@@ -82,13 +139,35 @@ $app_update_available = !empty($pending_commits);
|
||||
</div>
|
||||
<div class="col-md-3 col-6 mb-3">
|
||||
<small class="text-secondary text-uppercase">Commit</small>
|
||||
<div class="h5 mb-0"><code><?= escapeHtml(substr((string) $current_version, 0, 7)) ?></code></div>
|
||||
<div class="h5 mb-0"><code><?= $current_version === '' ? '—' : escapeHtml(substr((string) $current_version, 0, 7)) ?></code></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($update_queued_at)) { ?>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-fw fa-clock me-2"></i>An update was queued at
|
||||
<strong><?= escapeHtml($update_queued_at) ?></strong>.
|
||||
<?php if ($cron_is_running) { ?>
|
||||
Cron will start it within a minute; this page will show the new version once it finishes.
|
||||
<?php } else { ?>
|
||||
<strong>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>
|
||||
<?php } ?>
|
||||
|
||||
<?php if (!empty($update_job['cron_job_last_error'])) { ?>
|
||||
<div class="alert alert-warning">
|
||||
<h5><i class="fas fa-fw fa-exclamation-triangle me-2"></i>The last queued update did not finish</h5>
|
||||
<pre class="bg-dark text-white p-2 mt-2 mb-2"><?= escapeHtml($update_job['cron_job_last_error']) ?></pre>
|
||||
Recorded <?= escapeHtml((string) $update_job['cron_job_last_error_at']) ?>. Clear it from
|
||||
<a href="cron.php" class="alert-link">Maintenance > Cron</a> once it has been dealt with.
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<hr>
|
||||
|
||||
<?php if (!$app_update_available && !$db_update_available) { ?>
|
||||
<?php if ($shell_available && !$app_update_available && !$db_update_available) { ?>
|
||||
|
||||
<div class="text-center py-3">
|
||||
<i class="far fa-3x fa-smile-wink text-dark"></i>
|
||||
@@ -120,24 +199,57 @@ $app_update_available = !empty($pending_commits);
|
||||
</p>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($app_update_available) { ?>
|
||||
<?php if ($app_update_available || !$shell_available) { ?>
|
||||
<div class="mb-4">
|
||||
<h6 class="text-uppercase text-secondary">Application files</h6>
|
||||
<p class="mb-2">
|
||||
<?= count($pending_commits) ?> commit<?= count($pending_commits) === 1 ? '' : 's' ?>
|
||||
behind <code><?= escapeHtml("origin/$repo_branch") ?></code>.
|
||||
</p>
|
||||
<a class="btn btn-primary confirm-link" href="post.php?update&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-download me-2"></i>Update App
|
||||
</a>
|
||||
<a class="btn btn-outline-danger ms-2 confirm-link" href="post.php?update&force_update=1&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-hammer me-2"></i>Force Update
|
||||
<?php if ($app_update_available) { ?>
|
||||
<p class="mb-2">
|
||||
<?= count($pending_commits) ?> commit<?= count($pending_commits) === 1 ? '' : 's' ?>
|
||||
behind <code><?= escapeHtml("origin/$repo_branch") ?></code>.
|
||||
</p>
|
||||
<?php } else { ?>
|
||||
<p class="mb-2">
|
||||
This server cannot check <code><?= escapeHtml("origin/$repo_branch") ?></code>, so there may
|
||||
or may not be anything waiting. Queueing an update when there is nothing to do is harmless.
|
||||
</p>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($shell_available) { ?>
|
||||
<a class="btn btn-primary confirm-link" href="post.php?update&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-download me-2"></i>Update App
|
||||
</a>
|
||||
<a class="btn btn-outline-danger ms-2 confirm-link" href="post.php?update&force_update=1&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-hammer me-2"></i>Force Update
|
||||
</a>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($update_queue_available) { ?>
|
||||
<a class="btn btn-dark <?= $shell_available ? 'ms-2' : '' ?> confirm-link" href="post.php?queue_update&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-clock me-2"></i>Queue Update
|
||||
</a>
|
||||
<?php } ?>
|
||||
|
||||
<p class="text-muted mt-2 mb-0">
|
||||
<small>
|
||||
Update App runs <code>git pull</code>. Force Update discards every local change and resets
|
||||
the files to <code><?= escapeHtml("origin/$repo_branch") ?></code> - use it only when a
|
||||
normal update will not apply.
|
||||
<?php if ($shell_available) { ?>
|
||||
Update App runs <code>git pull</code>. Force Update discards every local change and resets
|
||||
the files to <code><?= escapeHtml("origin/$repo_branch") ?></code> - use it only when a
|
||||
normal update will not apply. Both run inside this request and stop if PHP runs out of time.
|
||||
<?php } ?>
|
||||
<?php if ($update_queue_available) { ?>
|
||||
Queue Update hands the job to cron, which runs
|
||||
<code>scripts/update_cli.php</code> in its own process - it updates the files and then the
|
||||
database, with no request timeout, as the user that owns the files. It resets the files to
|
||||
<code><?= escapeHtml("origin/$repo_branch") ?></code>, so local changes to them are lost.
|
||||
<?php if (!$cron_is_running) { ?>
|
||||
<strong class="text-warning">Cron is not checking in, so a queued update will sit there
|
||||
until it is.</strong>
|
||||
<?php } ?>
|
||||
<?php } else { ?>
|
||||
<strong>Apply the database update below first.</strong> Queueing an update needs a schema
|
||||
change this install has not caught up with yet. From a shell,
|
||||
<code>php scripts/update_cli.php</code> does both in one step.
|
||||
<?php } ?>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user