Files
ignacionelson da1f432d87 Let an installation stop calling home, two different ways
Every instance reached projectsend.org twice a day and an operator could
stop neither. The news feed had no switch of any kind — FetchNewsCommand
went straight to the request, touching Settings only to write results back.
The update check had one, but its default is on, and a managed fleet had
been setting PROJECTSEND_CHECK_FOR_UPDATES=false for months against code
that reads no such variable: check_for_updates is a database setting, so
the environment never touched it and updates were enabled fleet-wide the
whole time.

They look like one problem and are two, which is why they are fixed
differently.

**The news feed gets a Setting**, its own key, default on. A Cloud client
with view_news sees that card today — DashboardController gates it on the
permission alone, with a comment saying in as many words that it is both
editions and carries no capability. So switching it off is an operator's
choice rather than an edition's, and it must stay reachable everywhere.
Its own key rather than riding on check_for_updates because they are two
different wants: "do not tell me about releases" and "do not show me the
project's news" are asked separately, and an installation with no outbound
access at all wants both.

**The update check gets a capability guard**, ahead of the setting it
already had, and deliberately not a Setting of its own. On a managed
installation the result is unreachable rather than unwanted: the
dashboard's System card and the update UI are both gated on
Capability::SystemUpdates, which is Community-only, and the image is
chosen by whoever provisioned the instance. A Setting would encode a fact
about the edition as a preference — leaving it switchable back on per
tenant, buying a nightly call for a number no screen can draw, and putting
the reason in a provisioning script rather than beside the code. A
self-hosted install holds the capability and loses nothing: its own
setting still decides.

Both guards return success rather than failure. A scheduled task that was
asked not to run has not failed, and reporting it as one would put a red
line in the scheduler history every night for an installation behaving
exactly as configured.

The news switch is on the General settings screen, outside the
can_manage_updates block that hides the update toggle where the capability
is absent — a setting only reachable by editing a database row is a row,
not a switch. Seven tests, and the two that matter go red when either
guard is removed. Sixteen locales translated in the same commit rather
than left for the pass, since a release is close.
2026-09-08 01:27:08 -03:00

87 lines
3.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Modules\Platform\Updates\Console;
use App\Modules\Platform\Capabilities\Capability;
use App\Modules\Platform\Capabilities\CapabilityRegistry;
use App\Modules\Platform\Settings\Setting;
use App\Modules\Platform\Settings\Settings;
use App\Modules\Platform\Updates\CheckForUpdates;
use Illuminate\Console\Command;
/**
* Community edition only, once a day. The work itself lives in
* CheckForUpdates, which the settings screen's "check now" button calls
* too; this is the scheduled half, and the only thing it adds is the
* setting that switches the schedule off.
*
* That setting governs *this* command and not the service on purpose: an
* administrator who does not want a daily outbound call should still be
* able to ask the question themselves.
*
* There is still no in-app self-updater — nothing here downloads or
* applies anything. Applying an update is `update.sh` on a server
* install, or a new image on a container one.
*/
class CheckForUpdatesCommand extends Command
{
protected $signature = 'projectsend:check-for-updates';
protected $description = 'Check for a newer ProjectSend release and notify admins (Community edition, runs daily)';
public function __construct(
private readonly Settings $settings,
private readonly CheckForUpdates $check,
private readonly CapabilityRegistry $capabilities,
) {
parent::__construct();
}
public function handle(): int
{
// Ahead of the setting, and deliberately not a setting itself.
//
// A Setting says "the operator does not want this". The true
// statement on a managed installation is "there is nowhere for
// this to appear and nothing they could do about it": the
// dashboard's System card is gated on Capability::SystemUpdates
// (DashboardController), which is Community-only, so the answer
// this command fetches cannot be drawn on any screen — and the
// update UI is closed by the same capability, so it could not be
// acted on if it were. The image is chosen by whoever provisioned
// the instance.
//
// Encoding that as a preference would leave it switchable back on
// per tenant, which buys a nightly call to GitHub for a number
// nobody can see, and would leave the reason in a provisioning
// script rather than beside the code. A self-hosted installation
// holds the capability and loses nothing: its own setting below
// still decides.
if (! $this->capabilities->has(Capability::SystemUpdates)) {
$this->info('Update checks do not apply to this installation.');
return self::SUCCESS;
}
if ($this->settings->get(Setting::CheckForUpdates) !== true) {
$this->info('Update checks are disabled.');
return self::SUCCESS;
}
$result = $this->check->run();
if (! $result['ok']) {
$this->warn($result['message']);
return self::FAILURE;
}
$this->info($result['message']);
return self::SUCCESS;
}
}