Skip to content

Add in-app update check and upgrade for global admins - #1000

Open
knofte wants to merge 1 commit into
postfixadmin:masterfrom
knofte:add-in-app-upgrade
Open

Add in-app update check and upgrade for global admins#1000
knofte wants to merge 1 commit into
postfixadmin:masterfrom
knofte:add-in-app-upgrade

Conversation

@knofte

@knofte knofte commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #998
Depends on #999

Adds a web-based update check and upgrade page accessible to global admins. The page queries the GitHub Releases API to show available updates with full changelogs from the running version, and allows one-click download and installation of updates.

Features

  • Version check — compares $CONF['version'] against GitHub Releases API
  • Changelog — shows release notes for all versions between current and latest
  • One-click upgrade — downloads the self-contained release tarball and extracts it over the current installation
  • Safety checks before applying:
    • Verifies PHP PharData extension is available
    • Checks the installation directory is writable
    • Never overwrites config.local.php
    • Clears OPcache after update so PHP serves the new files
    • Prompts admin to confirm before applying
    • Reminds to run upgrade.php for database migrations
  • Graceful fallback — if no tarball asset exists (pre-Add GitHub Actions workflow to build self-contained release archives #999 releases), links to the GitHub release page instead
  • Menu integration — added to admin navbar, and footer "check update" link now points to this page for global admins

How it works

  1. Admin visits Update Check page (or clicks "check update" in footer)
  2. Page fetches releases from api.github.com/repos/postfixadmin/postfixadmin/releases
  3. Shows all newer versions with changelogs
  4. If a self-contained .tar.gz asset is available (from Add GitHub Actions workflow to build self-contained release archives #999), shows "Download & Apply" button
  5. On click: downloads tarball to sys_get_temp_dir(), extracts via PharData, copies files over installation, resets OPcache
  6. Redirects back with success message and link to upgrade.php

Files changed

  • public/update-check.php — new page with all update logic
  • templates/update-check.tpl — Smarty template with Bootstrap 5 UI
  • configs/menu.conf — added URL mapping
  • templates/menu.tpl — added menu item for global admins
  • templates/index.tpl — footer "check update" link points to new page for global admins

Test plan

  • Verify page loads and shows current version
  • Verify GitHub API returns release list with changelogs
  • Verify "up to date" message when running latest
  • Verify writable/PharData checks display appropriate warnings
  • Test upgrade flow with a self-contained release tarball (requires Add GitHub Actions workflow to build self-contained release archives #999)
  • Verify config.local.php is preserved during upgrade
  • Verify OPcache is cleared after upgrade

@knofte
knofte force-pushed the add-in-app-upgrade branch 4 times, most recently from 7b196d9 to 15539f6 Compare March 30, 2026 19:58
@knofte

knofte commented Mar 30, 2026

Copy link
Copy Markdown
Contributor Author

Local QA Results

Check Result
Lint PHP 8.4 Pass
Lint PHP 8.5 Pass
php-cs-fixer 0 issues
Psalm (our code) 0 issues (5 pre-existing in TotpPf.php)
PHPUnit 8.3 (sqlite + gd) 74 tests, 976 assertions, all pass
PHPUnit 8.5 (sqlite + gd) 74 tests, 976 assertions, all pass

@knofte

knofte commented Mar 30, 2026

Copy link
Copy Markdown
Contributor Author

Will rebase after PR #1001 is merged, and CI should pass nicely.

Adds an update-check page that queries the GitHub Releases API to show
available updates with changelogs, and allows global admins to download
and apply updates directly from the web UI.

Depends on postfixadmin#999 for self-contained release tarballs.
@knofte
knofte force-pushed the add-in-app-upgrade branch from 1d00db8 to c05750c Compare March 31, 2026 06:42
@DavidGoodwin

Copy link
Copy Markdown
Member

I'm tempted to request we remove the http calls via fopenwrappers/file_get_contents/fopen etc and use something like Guzzle ... but that would mean guzzle would become a hard dependency (it is currently pulled in via a dev dependency - phpcoveralls)

@knofte

knofte commented Mar 31, 2026

Copy link
Copy Markdown
Contributor Author

Yeah, adding Guzzle as a hard dependency just for this feels heavy. The current approach uses cURL as primary (available on most PHP installs) with file_get_contents as fallback, which should cover most setups without adding dependencies.

If you'd want to standardise HTTP calls across the project later, that could be a separate issue/PR. Happy to help with that too.

@DavidGoodwin

Copy link
Copy Markdown
Member

Right, I suspect the implementation of this PR is OK - although I've not tested it yet. I am however waiting a week or so before deciding if the change should be merged, and hoping e.g. @cboltz might chip in with an opinion.

I think most admins probably install postfixadmin through a package manager (e.g. via deb/rpm). Some may install it by following the web instructions. I suspect it'll normally then stay on a private/protected website, not be exposed to the internet and just do it's thing. I would imagine most people only update when they update the O/S ... or if a new package is published?

Because of that, I'm not totally sure if having an auto-update feature is desirable/wanted.....

(My arguments may be flawed. I'm hoping others might chip in opinions....)

Comment thread public/update-check.php
$install_dir = dirname(__DIR__);
$tmp_dir = sys_get_temp_dir() . '/postfixadmin-update-' . uniqid();

if (!mkdir($tmp_dir, 0700, true)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uniqid is predictable (time based) according https://www.php.net/uniqid which also means using it to create a tempdir could be a security risk.

A quick test shows that the if !mkdir can prevent symlink attacks (if the directory pre-exists as directory or symlink, mkdir will error out), so at least an attacker can "only" block the automated update.

Since I'm already commenting on this line - do you really think the true for recursively creating the directory is needed? IMHO it would be quite surprising if sys_get_temp_dir() points to a non-existing directory.

Comment thread public/update-check.php
$tmp_file = $tmp_base ? $tmp_base . '.tar.gz' : false;
if ($tmp_base === false || $tmp_file === false) {
flash_error('Failed to create temporary file.');
} elseif (!rename($tmp_base, $tmp_file)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://www.php.net/rename says that existing files will be overwritten. While it at least replaces an existing symlink (instead of giving a working symlink attack) (tested with PHP 8.4.19), this doesn't sound ideal. Maybe do the download into a temporary directory?

Comment thread public/update-check.php
opcache_reset();
}
flash_info("Successfully updated to version " . htmlspecialchars($update_version) . ". Please run <a href='upgrade.php'>upgrade.php</a> to apply any database changes.");
header('Location: update-check.php');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better send users directly to upgrade.php to enforce the database upgrade. Otherwise they might ignore the message ("hidden" in a success message) and in worst case hit random errors in various list or edit pages if an expected database change is missing.

Actually there's not really a need to send the users there - require('upgrade.php'); should work.

(We only run check_db_version() on login.)

<h4><span class="bi bi-arrow-repeat" aria-hidden="true"></span> {$PALANG.pUpdate_check_title|default:'Update Check'}</h4>
</div>
<div class="card-body">
<p><strong>{$PALANG.pUpdate_current_version|default:'Current version'}:</strong> {$current_version}</p>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using |default:, please use the language-update.sh script to add all needed texts to the language files.

Comment thread public/update-check.php
// Check the install directory is writable
$install_dir = dirname(__DIR__);
if (!is_writable($install_dir)) {
flash_error('Installation directory is not writable by the web server. Cannot apply update.');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check might be "good enough", but I can imagine several reasons why replacing some or all files might fail nevertheless:

  • files not owned by the webserver user (+ maybe sticky bit set on a directory)
  • subdirectories not owned by the webserver user
  • AppArmor or SELinux preventing write access.

Doing a "perfect" check is hard (and probably impossible when it comes to checking AppArmor or SELinux permissions), so your "good enough" solution might indeed be good enough.

@cboltz

cboltz commented Apr 1, 2026

Copy link
Copy Markdown
Member

Besides the technical details I mentioned above, let me add a general comment:

I don't like the idea to allow an webapp to update itsself. Not only because people might install PostfixAdmin via a rpm or deb. The main reason is because self-updating means all files need to be writable for the webserver, which is a security risk. If we really add this feature, it should be behind a config option which is disabled by default. (Personally I'd vote against adding the self-update feature.)

I know there are other PHP apps that contain a self-update feature, and I even understand why for example Wordpress self-update cronjobs make sense (attackers are faster than you can do manual updates), but PostfixAdmin is very different:

  • PostfixAdmin has a much saner security history
  • people who run a mailserver typically know how to upgrade PostfixAdmin manually - but I'm sure many people running a wordpress site don't know how to do an upgrade

Depending on how strict the server config is, even doing the update check with HTTP requests might be blocked (by a firewall blocking outgoing traffic and/or disable_functions) or trigger some alerts. This also means that the update check should be behind a config option which switches between your "fancy" update check and a boring link to the github releases page. (I'd be ok with enabling your "fancy" update check by default.)

@jmontesse

jmontesse commented Apr 2, 2026

Copy link
Copy Markdown

Because of that, I'm not totally sure if having an auto-update feature is desirable/wanted

knofte made a very good point that this comes down to sysadmin preference. Some want to be able to do a web-based "update me" method. It is quite easy, and can avoid some shell hassles on a lot of hosting services that are built around intra-app management.

For those sysadmins (of which I am one one) who use package managers and work primary via shell, it's a feature we wouldn't use. The point I made and cboltz echoed about traditional webserver security (read-only webroots) make it non-functional without any real consequence. A config.local.php knob that hides the feature would provide a useful safety barrier for us console cowpokes deploying the app via management systems. :-)

I think it's a useful feature, but one that needs to be fully optional and hideable.

@knofte

knofte commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback everyone. I've split this into two parts:

@TrapoSAMA

Copy link
Copy Markdown
Contributor

There is a better solution for this approach; I will create a PR with the proposal I have in mind.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add in-app upgrade button for administrators

5 participants