Files
fitme3070.pt/shared/platform-tools/bin/encrypt_secrets.php
T
andreandClaude Opus 5 4d40adbf64 Vendor platform-tools: everything this site runs is now in one repo
shared/platform-tools was a git submodule. On this branch it is a plain
directory of tracked files, frozen at ea3616abae30106df283e73a3e2f9e5bb0e6ae90
— the same commit fotografia.andmore.pt pins. Contents are byte-identical to
that tree, verified blob by blob.

The submodule caused every deployment failure this site has had:

  - .gitmodules named no `branch`, so the update fell back to
    refs/remotes/origin/HEAD — a ref a plain server-side clone never writes.
    "Atualizar submódulo" failed with "Unable to find refs/remotes/origin/HEAD
    revision in submodule path".
  - Its URL was http://, which the origin answers with 523.
  - This install is deployed by uploading files, and a submodule is invisible
    to that. The tree moved forward while shared/platform-tools stayed on the
    old pin, config/functions_secrets.php died on a missing compat_secrets.php,
    and the whole site — public pages included — went down.

Each of those was separately fixable. Together they mean a second repository
nobody can see from the deployment path, gating whether the first one boots. A
plain directory deploys like every other file and cannot fall out of step.

The cost is that platform-tools updates no longer arrive by bumping a pin; they
are a copy-over-and-commit. shared/platform-tools/VENDORED.md records the
upstream SHA, the procedure, and which four files differ from upstream in line
endings only (social/*.js, .css, functions_publications.php — CRLF upstream,
normalised to LF here; content otherwise identical).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 23:06:23 +01:00

90 lines
3.5 KiB
PHP

<?php
// Encrypt any secret-valued row in system_settings that is still plaintext.
//
// docker exec -u www-data <container> php \
// /var/www/html/shared/platform-tools/bin/encrypt_secrets.php [--dry-run]
//
// Idempotent -- rows already carrying the enc:v1: prefix are left alone -- so it
// is safe to call from deploy.sh on every deploy. Exits non-zero if a row that
// should be encrypted could not be, so a deploy notices.
//
// Bootstrapping differs per site: the config/ stack (dance, fotografia, hub2)
// opens $pdo in config/db.php; surf/silver builds it through App\Database. Try
// the common path first and fall back, rather than assuming either.
declare(strict_types=1);
$repoRoot = dirname(__DIR__, 3);
$dryRun = in_array('--dry-run', $argv, true);
$pdo = null;
if (is_file($repoRoot . '/config/db.php')) {
require_once $repoRoot . '/config/db.php'; // defines $pdo
if (is_file($repoRoot . '/config/functions.php')) {
require_once $repoRoot . '/config/functions.php';
}
} elseif (is_file($repoRoot . '/src/db.php')) {
// surf/silver: config.php loads the environment that db.php then reads.
if (is_file($repoRoot . '/src/config.php')) require_once $repoRoot . '/src/config.php';
require_once $repoRoot . '/src/db.php';
}
if (!$pdo instanceof PDO) {
foreach (['App\\Database::getConnection', 'App\\Database::pdo', 'db', 'get_pdo'] as $factory) {
if (is_callable($factory)) { $pdo = $factory(); break; }
}
}
if (!$pdo instanceof PDO) {
fwrite(STDERR, "encrypt_secrets: could not open a PDO connection for {$repoRoot}\n");
exit(2);
}
// src/db.php carries hardcoded development fallbacks (a 192.168.x host, a
// *_dev schema). If the environment did not reach it we would connect
// somewhere harmless-looking and "encrypt" nothing, reporting success. Refuse
// unless the live connection is the schema DB_NAME actually names.
$expected = (string) ($_ENV['DB_NAME'] ?? getenv('DB_NAME') ?: '');
$actual = (string) $pdo->query('SELECT DATABASE()')->fetchColumn();
if ($expected !== '' && $actual !== $expected) {
fwrite(STDERR, "encrypt_secrets: connected to '{$actual}' but DB_NAME is '{$expected}' -- refusing to run.\n");
exit(2);
}
require_once dirname(__DIR__) . '/functions_settings.php';
if (!platform_settings_secret_available()) {
fwrite(STDERR, "encrypt_secrets: no APP_SECRET_KEY (or BACKUP_TOKEN) configured -- refusing to run.\n");
exit(3);
}
if ($dryRun) {
// Report without writing: list secret-named rows still in plaintext.
$rows = $pdo->query("SELECT setting_key, setting_value FROM system_settings")->fetchAll(PDO::FETCH_ASSOC);
$pending = [];
foreach ($rows as $row) {
$k = (string) $row['setting_key'];
$v = (string) $row['setting_value'];
if (platform_settings_is_secret($k) && $v !== '' && !str_starts_with($v, PLATFORM_SECRET_PREFIX)) {
$pending[] = $k;
}
}
echo $pending
? "would encrypt: " . implode(', ', $pending) . "\n"
: "nothing to encrypt -- all secret rows are empty or already encrypted\n";
exit(0);
}
$report = platform_settings_encrypt_existing($pdo);
foreach ($report['encrypted'] as $k) echo "encrypted: {$k}\n";
foreach ($report['skipped'] as $k) echo "skipped: {$k}\n";
foreach ($report['failed'] as $k) fwrite(STDERR, "FAILED: {$k}\n");
printf(
"%d encrypted, %d skipped, %d failed\n",
count($report['encrypted']), count($report['skipped']), count($report['failed'])
);
exit($report['failed'] ? 1 : 0);