Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
705f22a
Heal every public/.htaccess Pinakes ever shipped, not only the 0.7.71…
fabiodalez-dev Oct 20, 2026
e4dc83e
Heal .htaccess files with CRLF line ends, and say so when the heal de…
fabiodalez-dev Oct 20, 2026
a370fe2
Count any spelling of the private uploads rule as safe
fabiodalez-dev Oct 20, 2026
eb7de34
Test the long flag names of the private uploads rule, and a rule with…
fabiodalez-dev Oct 20, 2026
80164ac
Leave a heal alone when an operator RewriteCond still waits above the…
fabiodalez-dev Oct 20, 2026
e0ef520
Run the patterns above the private rule instead of matching two spell…
fabiodalez-dev Oct 20, 2026
ad47c7d
Report a stopping passthrough unless its pattern provably misses the …
fabiodalez-dev Oct 20, 2026
1bfc4f8
Keep the no-gzip update when the rewrite heal declines
fabiodalez-dev Oct 20, 2026
572ad97
Count a rule chained into the private one as a condition
fabiodalez-dev Oct 20, 2026
f96e474
Test Digital Library files uploaded on an older release across the up…
fabiodalez-dev Oct 20, 2026
62a5144
Cover the one-file eBook and audiobook fields of releases before 0.7.…
fabiodalez-dev Oct 20, 2026
efc14a2
Sync the existing tables of an inactive bundled plugin when its versi…
fabiodalez-dev Oct 20, 2026
af22673
Re-register the Digital Library hooks on old releases that installed …
fabiodalez-dev Oct 20, 2026
a07aba1
Sync an inactive plugin's tables even when its version bump is alread…
fabiodalez-dev Oct 20, 2026
5789146
Give the real cadence of the inactive plugin schema pass
fabiodalez-dev Oct 20, 2026
0cbbed3
Skip the upgrade spec without a phase under a reason the skip audit a…
fabiodalez-dev Oct 20, 2026
233565b
Keep the schema-only instance of a switched-off plugin from registeri…
fabiodalez-dev Oct 20, 2026
a099335
Keep public/.htaccess when PCRE gives up, accept a private rule endin…
fabiodalez-dev Oct 20, 2026
13dbdd4
Report an Apache file PCRE could not read, cover a replace that gives…
fabiodalez-dev Oct 20, 2026
716221c
Keep the checked rewrite heal when only the no-gzip replace gives up
fabiodalez-dev Oct 20, 2026
fc28895
test: cover upload rewrite backup and filesystem failures
fabiodalez-dev Oct 20, 2026
22d06f7
Do not read a PCRE give-up in the public rule search as "no public rule"
fabiodalez-dev Oct 20, 2026
185dd4f
Read a lower-case RewriteRule as a rewrite rule in acceptable()
fabiodalez-dev Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ tests/fixtures/*
!tests/fixtures/*.csv
!tests/fixtures/*.tsv
!tests/fixtures/sru/
!tests/fixtures/htaccess-history/
!tests/helpers/
tests/helpers/*
!tests/helpers/*.php
Expand Down
67 changes: 62 additions & 5 deletions app/Support/PluginManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -483,9 +483,21 @@ public function autoRegisterBundledPlugins(): int
// that is behind. Each plugin now syncs independently.
SecureLogger::error("[PluginManager] onActivate failed during upgrade for $pluginName; version rolled back to $dbVersion, other plugins keep syncing, retry next boot. Error: " . $e->getMessage());
}
} else {
$this->syncInactivePluginSchema((int) $row['id'], $pluginName, $pluginMeta);
}
} elseif ($diskVersion === $dbVersion && (int) ($row['is_active'] ?? 0) === 1) {
// Same disk/DB version, active plugin. Re-run onActivate when
} elseif ((int) ($row['is_active'] ?? 0) !== 1) {
// Same version, but the bump may already have been taken by the
// previous release's Updater, which runs this sync with its own
// PluginManager still in memory: an install upgraded from 0.7.16
// arrives here with NCIP at 1.0.5 and prenotazione_id missing.
// This pass runs on every maintenance window (900 s per version
// stamp) and on every getAllPlugins() call (each load of the admin
// plugins page), and the check is read-only until something is
// actually missing.
$this->syncInactivePluginSchema((int) $row['id'], $pluginName, $pluginMeta);
} elseif ($diskVersion === $dbVersion) {
// Same disk/DB version, active plugin (the inactive ones took the branch above). Re-run onActivate when
// EITHER:
// (a) hooks are missing — code added hooks not yet in DB
// (e.g. merging branches that extend the same version), or
Expand Down Expand Up @@ -2317,7 +2329,7 @@ public function loadActivePlugins(): void
// adds [NC] to the private-uploads rewrite. Idempotent; a file the operator
// edited by hand is left alone.
if (!PrivateUploadsRewriteHeal::heal()) {
SecureLogger::error('[PluginManager] public/.htaccess not updated for #480: add [NC] to the RewriteRule ^uploads/(digital|archives/documents|storage)/ line by hand');
SecureLogger::error('[PluginManager] public/.htaccess still lets private uploads be served from disk (#480): put "RewriteRule ^uploads/(digital|archives/documents|storage)/ index.php [NC,QSA,L]" before the public /uploads/ rule, or restore the shipped file');
}

QueryCache::set($maintenanceKey, 1, 900);
Expand Down Expand Up @@ -2748,6 +2760,45 @@ public static function clearPluginCache(): void
SitemapCache::invalidate('plugin lifecycle change');
}

/**
* An inactive plugin whose tables already exist only gets them synced by
* onActivate: a column or foreign key a later version adds stays missing until
* someone switches the plugin on (ncip_transactions.prenotazione_id on an
* install upgraded from 0.7.x). Bring those tables to this version without
* registering any hook. A plugin whose tables are not there (never installed,
* like an untouched Archives) is left alone: activating it creates them.
*
* @param array<string, mixed> $pluginMeta
*/
private function syncInactivePluginSchema(int $pluginId, string $pluginName, array $pluginMeta): void
{
try {
// Schema only: no setPluginId(), which would register the hooks of a
// plugin that is switched off (or, for digital-library, move its files).
$schemaInstance = $this->instantiatePlugin([
'id' => $pluginId,
'name' => $pluginName,
'path' => $pluginName,
'main_file' => $pluginMeta['main_file'] ?? 'wrapper.php',
], false);
// is_callable, not method_exists: most wrappers forward to the
// plugin class through __call.
$declared = is_callable([$schemaInstance, 'expectedTables']) ? $schemaInstance->expectedTables() : [];
Comment thread
fabiodalez-dev marked this conversation as resolved.
$tablesInstalled = is_array($declared) && $declared !== []
&& !$this->expectedTablesMissing($schemaInstance);
if ($tablesInstalled && is_callable([$schemaInstance, 'ensureSchema'])
&& ($this->expectedColumnsMissing($schemaInstance) || $this->expectedForeignKeysMissing($schemaInstance))) {
$schemaResult = $schemaInstance->ensureSchema();
if (is_array($schemaResult) && !empty($schemaResult['failed'])) {
SecureLogger::warning("[PluginManager] Schema sync of inactive $pluginName left: " . implode(', ', (array) $schemaResult['failed']));
}
}
} catch (\Throwable $e) {
// Not fatal: activating the plugin runs ensureSchema again.
SecureLogger::warning("[PluginManager] Schema sync of inactive $pluginName failed: " . $e->getMessage());
}
}

/**
* Version stamp used to scope the maintenance marker: a new release (new
* version.json) invalidates it so bundled plugins re-sync right away.
Expand Down Expand Up @@ -2871,7 +2922,13 @@ private function disableOrphanHook(int $pluginId, string $hookName, string $call
return $affected > 0;
}

private function instantiatePlugin(array $plugin): object
/**
* Build a plugin instance. $setPluginId false gives a schema-only instance: some
* plugins (digital-library, open-library, goodlib, scraping-pro) register their
* hooks in setPluginId(), and digital-library also moves its files, which a
* plugin the admin switched off must not do. No ensureSchema() reads the id.
*/
private function instantiatePlugin(array $plugin, bool $setPluginId = true): object
{
// Save plugin data to prefixed variables before require_once
// This prevents plugin files from overwriting $plugin variable (which some do)
Expand All @@ -2893,7 +2950,7 @@ private function instantiatePlugin(array $plugin): object

$instance = new $className($this->db, $this->hookManager);

if (is_callable([$instance, 'setPluginId'])) {
if ($setPluginId && is_callable([$instance, 'setPluginId'])) {
try {
$instance->setPluginId($_pluginId);
} catch (\Throwable $e) {
Expand Down
Loading
Loading