mirror of
https://github.com/itflow-org/itflow
synced 2026-08-31 11:55:11 +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:
@@ -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: <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
|
||||
* 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";
|
||||
|
||||
Reference in New Issue
Block a user