diff --git a/CLAUDE.md b/CLAUDE.md index b8ae706..405ffb2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,3 +69,20 @@ All use `CREATE TABLE IF NOT EXISTS` for safe coexistence with existing Proclaim ## Build Run `php build/build.php` to create the distributable `pkg_cwmscripture-{version}.zip`. The build script pulls the library from the submodule directory. Tests live in the `lib_cwmscripture` repo. + +### Package scriptfile + +`build/script.install.php` is the package's install script (wired via `` +in `build/pkg_cwmscripture.xml` and `package.installer` in `cwm-build.config.json`). + +Its one job is to blank `JPATH_LIBRARIES/cwmscripture/sql/uninstall.mysql.utf8.sql` +in `preflight()`, before the library child installs. lib_cwmscripture ≤ 1.1.4 +declared `` pointing at `DROP TABLE` statements, and Joomla's +`LibraryAdapter` uninstalls the installed library on every update — so those DROPs +ran on upgrades and wiped every locally downloaded Bible translation. The library +no longer declares uninstall SQL, but Joomla reads the *installed* manifest and SQL +file during that uninstall, so only a package-level preflight can save an existing +site on the transition upgrade. + +Bump the nested `libraries/lib_cwmscripture` submodule to a version ≥ the fix, or +this package keeps reinstalling the destructive manifest on disk. diff --git a/build/pkg_cwmscripture.xml b/build/pkg_cwmscripture.xml index e18f153..bfc3bf4 100644 --- a/build/pkg_cwmscripture.xml +++ b/build/pkg_cwmscripture.xml @@ -11,6 +11,16 @@ 1.1.5 CWM Scripture package: Bible provider library, content plugin for automatic scripture linking, and task plugin for deferred background scripture work. + script.install.php + + + true + https://raw.githubusercontent.com/Joomla-Bible-Study/CWMScriptureLinks/main/build/cwmscripture-changelog.xml @@ -22,5 +32,6 @@ lib_cwmscripture.zip plg_content_scripturelinks.zip plg_task_cwmscripture.zip + plg_system_cwmscripture.zip diff --git a/build/script.install.php b/build/script.install.php new file mode 100644 index 0000000..5ca0025 --- /dev/null +++ b/build/script.install.php @@ -0,0 +1,304 @@ +disarmLegacyScriptureUninstallSql(); + + return true; + } + + /** + * Runs after install/update completes. + * + * @param string $type Install type (install, update, discover_install) + * @param InstallerAdapter $adapter The installer adapter + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function postflight(string $type, InstallerAdapter $adapter): bool + { + // The protection plugin is useless disabled, and Joomla installs plugins + // disabled by default — see the method docblock. + $this->enablePlugin('cwmscripture', 'system'); + + return true; + } + + /** + * Enable a bundled plugin. + * + * Joomla installs plugins disabled, which is wrong for + * plg_system_cwmscripture: its whole job is to blank the scripture library's + * legacy uninstall SQL before an admin runs a library-only update, and a + * disabled plugin never fires. Leaving it to the administrator would mean the + * protection is off exactly on the sites that have not been touched recently + * — the ones most likely to still be on a destructive 1.1.4 library. + * + * Only flips a plugin that is currently disabled, so an administrator who + * deliberately turned it off is not overridden on the next update. + * + * @param string $element Plugin element + * @param string $group Plugin group + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function enablePlugin(string $element, string $group): void + { + try { + $db = Factory::getContainer()->get(DatabaseInterface::class); + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('element') . ' = ' . $db->quote($element)) + ->where($db->quoteName('folder') . ' = ' . $db->quote($group)) + ->where($db->quoteName('enabled') . ' = 0'); + $db->setQuery($query); + $db->execute(); + } catch (\Throwable $e) { + Log::add( + 'pkg_cwmscripture: could not enable plg_' . $group . '_' . $element . ' — ' . $e->getMessage(), + Log::WARNING, + 'jerror' + ); + } + } + + /** + * Runs on install. + * + * @param InstallerAdapter $adapter The installer adapter + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function install(InstallerAdapter $adapter): bool + { + return true; + } + + /** + * Runs on update. + * + * @param InstallerAdapter $adapter The installer adapter + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function update(InstallerAdapter $adapter): bool + { + return true; + } + + /** + * Runs on uninstall. + * + * @param InstallerAdapter $adapter The installer adapter + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function uninstall(InstallerAdapter $adapter): bool + { + $this->dropScriptureTablesIfLastConsumer(); + + return true; + } + + /** + * Remove the shared scripture tables when this package is the last consumer. + * + * The library deliberately will not do this itself during a package removal: + * PackageAdapter::removeExtensionFiles() uninstalls each child with + * setPackageUninstall(true), and lib_cwmscripture treats that flag as "an + * upgrade cycle may be in progress, keep the data". Correct for the library, + * but it means nothing cleans up when the whole package genuinely goes away. + * + * Doing it here is safe because PackageAdapter does NOT uninstall-then-install + * on update — checkExtensionInFilesystem() only sets the route — so a package + * manifest script's uninstall() runs on genuine removal and never on upgrade. + * It also runs before removeExtensionFiles(), so the children are still + * present and the drop happens exactly once. + * + * The "is anything else using it" question is delegated to the library's + * ConsumerRegistry rather than answered with a hardcoded list. Joomla tracks + * no library dependencies, so a third-party extension is invisible unless it + * registered itself; asking the registry means such a consumer is seen and + * its data left alone, instead of being silently dropped. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function dropScriptureTablesIfLastConsumer(): void + { + try { + $registry = JPATH_LIBRARIES . '/cwmscripture/src/Installer/ConsumerRegistry.php'; + + if (!class_exists(ConsumerRegistry::class) && is_file($registry)) { + require_once $registry; + } + + if (!class_exists(ConsumerRegistry::class)) { + Log::add( + 'pkg_cwmscripture: ConsumerRegistry unavailable — keeping the scripture tables.', + Log::WARNING, + 'jerror' + ); + + return; + } + + // Everything this package removes; anything left is someone else's. + $remaining = ConsumerRegistry::installedExcluding([ + ['element' => 'scripturelinks', 'type' => 'plugin', 'folder' => 'content'], + ['element' => 'cwmscripture', 'type' => 'plugin', 'folder' => 'task'], + ]); + + if ($remaining !== []) { + Log::add( + 'pkg_cwmscripture: scripture tables still used by ' . implode(', ', $remaining) . ' — keeping them.', + Log::INFO, + 'jerror' + ); + + return; + } + + $db = Factory::getContainer()->get(DatabaseInterface::class); + + foreach (['#__bsms_scripture_consumers', '#__bsms_scripture_cache', '#__bsms_bible_verses', '#__bsms_bible_translations'] as $table) { + $db->setQuery('DROP TABLE IF EXISTS ' . $db->quoteName($table)); + $db->execute(); + } + + Log::add( + 'pkg_cwmscripture: removed the scripture tables (last consumer uninstalled).', + Log::INFO, + 'jerror' + ); + } catch (\Throwable $e) { + Log::add( + 'pkg_cwmscripture: scripture table cleanup skipped — ' . $e->getMessage(), + Log::WARNING, + 'jerror' + ); + } + } + + /** + * Stop the installed scripture library from dropping its own tables. + * + * lib_cwmscripture up to 1.1.4 shipped an block pointing at + * DROP TABLE statements. Joomla's LibraryAdapter uninstalls the installed + * library before writing the new one (checkExtensionInFilesystem() calls + * uninstall()), so that SQL ran on every UPDATE — taking #__bsms_bible_verses + * and #__bsms_bible_translations with it. Every locally downloaded + * translation disappeared and the Local Translations panel came back empty. + * + * The library no longer declares uninstall SQL, but that only helps the + * upgrade *after* this one: during this install Joomla reads the manifest and + * the SQL file already sitting in JPATH_LIBRARIES — the old, destructive + * ones. Installer::parseSQLFiles() resolves the file against the installed + * extension root, so blanking it here, before the library child installs, is + * what actually saves the data. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function disarmLegacyScriptureUninstallSql(): void + { + $sqlFile = JPATH_LIBRARIES . '/cwmscripture/sql/uninstall.mysql.utf8.sql'; + + if (!is_file($sqlFile)) { + return; + } + + $buffer = @file_get_contents($sqlFile); + + if ($buffer === false || stripos($buffer, 'DROP TABLE') === false) { + return; + } + + $replacement = "--\n" + . "-- Neutralised by the pkg_cwmscripture installer.\n" + . "--\n" + . "-- Joomla runs a library's uninstall SQL on every update, so the DROP TABLE\n" + . "-- statements this file used to hold wiped every downloaded Bible translation\n" + . "-- each time lib_cwmscripture was upgraded. Table removal now lives in the\n" + . "-- library's script.php, which can tell an upgrade from a real uninstall.\n" + . "--\n"; + + if (@file_put_contents($sqlFile, $replacement) === false) { + Factory::getApplication()->enqueueMessage( + 'The scripture package could not rewrite ' . $sqlFile . '. Joomla may drop your ' + . 'locally downloaded Bible translations during this update; re-download them ' + . 'from the Scripture Links plugin settings if the Local Translations list comes ' + . 'back empty.', + 'warning' + ); + + Log::add( + 'pkg_cwmscripture: could not disarm legacy scripture uninstall SQL at ' . $sqlFile, + Log::WARNING, + 'cwmscripture.install' + ); + + return; + } + + Log::add( + 'pkg_cwmscripture: disarmed legacy scripture uninstall SQL (bible tables preserved).', + Log::INFO, + 'cwmscripture.install' + ); + } +}; diff --git a/composer.json b/composer.json index 1a8656a..6eae8b9 100644 --- a/composer.json +++ b/composer.json @@ -1,66 +1,78 @@ { - "name": "cwm/scripture-links", - "description": "CWM Scripture Links - Content plugin for automatic scripture linking in Joomla 5/6", - "type": "joomla-package", - "license": "GPL-2.0-or-later", - "homepage": "https://www.christianwebministries.org", - "require": { - "php": ">=8.4" - }, - "require-dev": { - "cwm/build-tools": "^1.9.0", - "friendsofphp/php-cs-fixer": "^3.0", - "phpunit/phpunit": "^12.5" - }, - "extra": { - "cwm-build-tools": { - "joomlaLinks": [ - { "type": "plugin", "group": "content", "element": "scripturelinks" }, - { "type": "plugin", "group": "task", "element": "cwmscripture" } - ] + "name": "cwm/scripture-links", + "description": "CWM Scripture Links - Content plugin for automatic scripture linking in Joomla 5/6", + "type": "joomla-package", + "license": "GPL-2.0-or-later", + "homepage": "https://www.christianwebministries.org", + "require": { + "php": ">=8.4" + }, + "require-dev": { + "cwm/build-tools": "^1.9.0", + "friendsofphp/php-cs-fixer": "^3.0", + "phpunit/phpunit": "^12.5" + }, + "extra": { + "cwm-build-tools": { + "joomlaLinks": [ + { + "type": "plugin", + "group": "content", + "element": "scripturelinks" + }, + { + "type": "plugin", + "group": "task", + "element": "cwmscripture" + } + ] + } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/Joomla-Bible-Study/cwm-build-tools" + } + ], + "autoload": { + "psr-4": { + "CWM\\Library\\Scripture\\": "libraries/lib_cwmscripture/src/", + "CWM\\Plugin\\Content\\ScriptureLinks\\": "src/", + "CWM\\Plugin\\Task\\Cwmscripture\\": "plg_task_cwmscripture/src/", + "CWM\\Plugin\\System\\Cwmscripture\\": "plg_system_cwmscripture/src/" + } + }, + "autoload-dev": { + "psr-4": { + "CWM\\ScriptureLinks\\Tests\\": "tests/unit/" + } + }, + "scripts": { + "build": "cwm-package", + "test": "phpunit", + "test:unit": "phpunit --testsuite unit", + "lint": "php-cs-fixer fix --dry-run --diff", + "lint:fix": "php-cs-fixer fix", + "lint:syntax": "find src plg_task_cwmscripture -type f -name '*.php' -print0 | xargs -0 -n 1 -P 8 php -l > /dev/null", + "check": [ + "@lint:syntax", + "@lint", + "@test" + ], + "release": "cwm-release", + "bump-version": "cwm-bump", + "package": "cwm-package", + "ars-publish": "cwm-ars-publish", + "ars-list": "cwm-ars-list", + "changelog": "cwm-changelog", + "sync-configs": "cwm-sync-configs", + "sync-languages": "cwm-sync-languages", + "setup": "cwm-setup", + "link": "cwm-link", + "link-check": "cwm-link-check", + "clean": "cwm-clean", + "verify": "cwm-verify", + "joomla-install": "cwm-joomla-install", + "joomla-latest": "cwm-joomla-latest" } - }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/Joomla-Bible-Study/cwm-build-tools" - } - ], - "autoload": { - "psr-4": { - "CWM\\Library\\Scripture\\": "libraries/lib_cwmscripture/src/", - "CWM\\Plugin\\Content\\ScriptureLinks\\": "src/", - "CWM\\Plugin\\Task\\Cwmscripture\\": "plg_task_cwmscripture/src/" - } - }, - "autoload-dev": { - "psr-4": { - "CWM\\ScriptureLinks\\Tests\\": "tests/unit/" - } - }, - "scripts": { - "build": "cwm-package", - "test": "phpunit", - "test:unit": "phpunit --testsuite unit", - "lint": "php-cs-fixer fix --dry-run --diff", - "lint:fix": "php-cs-fixer fix", - "lint:syntax": "find src plg_task_cwmscripture -type f -name '*.php' -print0 | xargs -0 -n 1 -P 8 php -l > /dev/null", - "check": ["@lint:syntax", "@lint", "@test"], - "release": "cwm-release", - "bump-version": "cwm-bump", - "package": "cwm-package", - "ars-publish": "cwm-ars-publish", - "ars-list": "cwm-ars-list", - "changelog": "cwm-changelog", - "sync-configs": "cwm-sync-configs", - "sync-languages": "cwm-sync-languages", - - "setup": "cwm-setup", - "link": "cwm-link", - "link-check": "cwm-link-check", - "clean": "cwm-clean", - "verify": "cwm-verify", - "joomla-install": "cwm-joomla-install", - "joomla-latest": "cwm-joomla-latest" - } } diff --git a/cwm-build.config.json b/cwm-build.config.json index 21321c7..4aac8bd 100644 --- a/cwm-build.config.json +++ b/cwm-build.config.json @@ -18,6 +18,7 @@ "outputDir": "build/dist", "outputName": "pkg_cwmscripture-{version}.zip", "innerLayout": "root", + "installer": "build/script.install.php", "includes": [ { "type": "prebuilt", @@ -56,14 +57,31 @@ ], "excludes": [".git", ".DS_Store", ".idea", "node_modules"] } + }, + { + "type": "inline", + "outputName": "plg_system_cwmscripture.zip", + "config": { + "outputDir": "build/dist", + "outputName": "plg_system_cwmscripture.zip", + "manifest": "plg_system_cwmscripture/cwmscripture.xml", + "sources": [ + { "from": "plg_system_cwmscripture/src", "to": "src" }, + { "from": "plg_system_cwmscripture/services", "to": "services" }, + { "from": "plg_system_cwmscripture/language", "to": "language" } + ], + "excludes": [".git", ".DS_Store", ".idea", "node_modules"] + } } ], "verify": { "expectedEntries": [ "pkg_cwmscripture.xml", + "script.install.php", "lib_cwmscripture.zip", "plg_content_scripturelinks.zip", - "plg_task_cwmscripture.zip" + "plg_task_cwmscripture.zip", + "plg_system_cwmscripture.zip" ] } }, diff --git a/libraries/lib_cwmscripture b/libraries/lib_cwmscripture index 3babb2a..addc517 160000 --- a/libraries/lib_cwmscripture +++ b/libraries/lib_cwmscripture @@ -1 +1 @@ -Subproject commit 3babb2ab9a5ab0204fe4cf14d902017e6afb0779 +Subproject commit addc517eb7e9ca93f2d79f9e343d56afde7a885f diff --git a/phpunit.xml b/phpunit.xml index 9491a44..91543d3 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -13,6 +13,7 @@ src plg_task_cwmscripture/src + plg_system_cwmscripture/src diff --git a/plg_system_cwmscripture/cwmscripture.xml b/plg_system_cwmscripture/cwmscripture.xml new file mode 100644 index 0000000..9f53dc2 --- /dev/null +++ b/plg_system_cwmscripture/cwmscripture.xml @@ -0,0 +1,24 @@ + + + plg_system_cwmscripture + CWM Team + info@christianwebministries.org + https://www.christianwebministries.org + (C) 2026 CWM Team All rights reserved + GNU General Public License version 2 or later; see LICENSE.txt + 1.1.6 + July 2026 + PLG_SYSTEM_CWMSCRIPTURE_XML_DESCRIPTION + CWM\Plugin\System\Cwmscripture + + + services + src + language + + + + language/en-GB/en-GB.plg_system_cwmscripture.ini + language/en-GB/en-GB.plg_system_cwmscripture.sys.ini + + diff --git a/plg_system_cwmscripture/language/en-GB/en-GB.plg_system_cwmscripture.ini b/plg_system_cwmscripture/language/en-GB/en-GB.plg_system_cwmscripture.ini new file mode 100644 index 0000000..db8ccc5 --- /dev/null +++ b/plg_system_cwmscripture/language/en-GB/en-GB.plg_system_cwmscripture.ini @@ -0,0 +1,6 @@ +; CWM Scripture Library - System Plugin +; Copyright (C) 2026 CWM Team All rights reserved +; License GNU General Public License version 2 or later; see LICENSE.txt + +PLG_SYSTEM_CWMSCRIPTURE="System - CWM Scripture Protection" +PLG_SYSTEM_CWMSCRIPTURE_XML_DESCRIPTION="Protects locally downloaded Bible translations from lib_cwmscripture's legacy uninstall SQL, which older versions ran on every update. Keep this plugin enabled." diff --git a/plg_system_cwmscripture/language/en-GB/en-GB.plg_system_cwmscripture.sys.ini b/plg_system_cwmscripture/language/en-GB/en-GB.plg_system_cwmscripture.sys.ini new file mode 100644 index 0000000..db8ccc5 --- /dev/null +++ b/plg_system_cwmscripture/language/en-GB/en-GB.plg_system_cwmscripture.sys.ini @@ -0,0 +1,6 @@ +; CWM Scripture Library - System Plugin +; Copyright (C) 2026 CWM Team All rights reserved +; License GNU General Public License version 2 or later; see LICENSE.txt + +PLG_SYSTEM_CWMSCRIPTURE="System - CWM Scripture Protection" +PLG_SYSTEM_CWMSCRIPTURE_XML_DESCRIPTION="Protects locally downloaded Bible translations from lib_cwmscripture's legacy uninstall SQL, which older versions ran on every update. Keep this plugin enabled." diff --git a/plg_system_cwmscripture/services/provider.php b/plg_system_cwmscripture/services/provider.php new file mode 100644 index 0000000..9e56179 --- /dev/null +++ b/plg_system_cwmscripture/services/provider.php @@ -0,0 +1,36 @@ +set( + PluginInterface::class, + function (Container $container) { + $plugin = new Cwmscripture( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('system', 'cwmscripture') + ); + + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/plg_system_cwmscripture/src/Extension/Cwmscripture.php b/plg_system_cwmscripture/src/Extension/Cwmscripture.php new file mode 100644 index 0000000..ba17cbf --- /dev/null +++ b/plg_system_cwmscripture/src/Extension/Cwmscripture.php @@ -0,0 +1,173 @@ +` pointing at DROP + * TABLE statements. Joomla's LibraryAdapter uninstalls the installed library + * before writing the new one, so that SQL ran on every UPDATE and wiped + * `#__bsms_bible_verses` / `#__bsms_bible_translations` — every translation the + * site owner had downloaded, gone, with the Local Translations panel coming + * back empty. + * + * 1.1.6 removed the block, but that only protects the upgrade *after* this one: + * the uninstall SQL Joomla executes during an update is the one already sitting + * in JPATH_LIBRARIES, belonging to the installed version. So a site on 1.1.4 is + * still destroyed by the very update that fixes it. + * + * The package installers disarm the file for package updates, but the library + * carries its own update server and is listed separately in the Update Manager. + * A library-only update runs no package code at all — and nothing inside the + * incoming library can help either, because `InstallerAdapter::install()` calls + * `checkExtensionInFilesystem()` (which triggers the old uninstall) *before* + * `triggerManifestScript('preflight')` loads the new script file. By the time + * any incoming code runs, the tables are already gone. + * + * The only remaining place to intervene is code already resident on the site, + * running before the installer does. Hence this plugin: it blanks the file + * while the administrator is on com_installer, before they click Update. + * + * plg_system_proclaim does the same thing for Proclaim sites. This exists so + * stacks that carry the library without Proclaim — Living Word, or any + * third-party consumer — are covered too. + * + * One-shot: once the file holds no statements the check short-circuits and this + * costs a single is_file()/strpos() per admin installer page load. + * + * @since __DEPLOY_VERSION__ + */ +final class Cwmscripture extends CMSPlugin implements SubscriberInterface +{ + /** + * Returns the events this subscriber will listen to. + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + public static function getSubscribedEvents(): array + { + return [ + 'onAfterRoute' => 'onAfterRoute', + ]; + } + + /** + * Disarm the legacy uninstall SQL while the admin is in the installer. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function onAfterRoute(): void + { + $app = $this->getApplication(); + + if (!$app->isClient('administrator')) { + return; + } + + if ($app->getInput()->getCmd('option', '') !== 'com_installer') { + return; + } + + $this->disarmLegacyScriptureUninstallSql(); + } + + /** + * Does this SQL contain an executable DROP TABLE? + * + * The distinction matters: the already-disarmed 1.1.5 and 1.1.6 files + * *describe* the old behaviour in prose, so they contain the words "DROP + * TABLE" inside `--` comments. A naive substring search flags them as + * dangerous and rewrites files that are perfectly fine — so only lines that + * are not comments count. + * + * Deliberately conservative: a trailing `-- DROP TABLE` comment on a real + * statement line reads as armed. Rewriting an already-safe file costs + * nothing; missing a live one costs the site its Bibles. + * + * @param string $sql Contents of an uninstall SQL file + * + * @return bool True when at least one executable DROP TABLE is present + * + * @since __DEPLOY_VERSION__ + */ + public static function sqlIsArmed(string $sql): bool + { + if (stripos($sql, 'DROP TABLE') === false) { + return false; + } + + foreach (preg_split('/\R/', $sql) ?: [] as $line) { + $line = trim($line); + + if ($line === '' || str_starts_with($line, '--')) { + continue; + } + + if (stripos($line, 'DROP TABLE') !== false) { + return true; + } + } + + return false; + } + + /** + * Blank the scripture library's legacy, destructive uninstall SQL. + * + * Rewrites rather than deletes: an older manifest still references the file + * by path, and Installer::parseSQLFiles() resolves it against the installed + * extension root, so removing it would make those sites fail to find it. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function disarmLegacyScriptureUninstallSql(): void + { + $sqlFile = JPATH_LIBRARIES . '/cwmscripture/sql/uninstall.mysql.utf8.sql'; + + if (!is_file($sqlFile) || !is_writable($sqlFile)) { + return; + } + + $contents = @file_get_contents($sqlFile); + + if ($contents === false || !self::sqlIsArmed($contents)) { + return; + } + + $replacement = <<<'SQL' +-- +-- CWM Scripture Library - Uninstall SQL (neutralised by plg_system_cwmscripture) +-- +-- This file held DROP TABLE statements for the shared bible tables. Joomla runs +-- a library's uninstall SQL on every UPDATE, not only on removal, so they were +-- destroying every locally downloaded translation on each upgrade. +-- +-- Emptied in place rather than deleted: an older manifest still references this +-- path. Do not reintroduce DROP TABLE statements here. +-- +SQL; + + @file_put_contents($sqlFile, $replacement . "\n"); + } +} \ No newline at end of file diff --git a/plg_task_cwmscripture/script.php b/plg_task_cwmscripture/script.php index c311296..586975d 100644 --- a/plg_task_cwmscripture/script.php +++ b/plg_task_cwmscripture/script.php @@ -38,9 +38,47 @@ public function update(InstallerAdapter $adapter): bool public function uninstall(InstallerAdapter $adapter): bool { + $this->consumer('unregister'); + return true; } + /** + * Declare, or withdraw, this plugin's dependency on lib_cwmscripture. + * + * Joomla tracks no dependencies between extensions, so the library cannot + * discover who relies on it. Registering means the library refuses to be + * uninstalled while we are installed, and the shared #__bsms_bible_* tables + * are not dropped while we still read them. + * + * The library's own entry point handles autoloading, version tolerance and + * error handling, so this stays two lines. Note the group is part of the + * registry key — a plugin registered without it never matches on lookup. + * + * @param string $action 'register' on install/update, 'unregister' on removal + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function consumer(string $action): void + { + if (!is_file($helper = JPATH_LIBRARIES . '/cwmscripture/src/consumer.php')) { + return; + } + + $registry = require $helper; + + // Not one dynamic call: unregister() takes no display name. + if ($action === 'register') { + $registry->register('cwmscripture', 'plugin', 'task', 'Scripture task plugin (plg_task_cwmscripture)'); + + return; + } + + $registry->unregister('cwmscripture', 'plugin', 'task'); + } + /** * Auto-enable the plugin and queue a one-shot core download task. * @@ -76,6 +114,7 @@ public function postflight(string $type, InstallerAdapter $adapter): bool $db->execute(); $this->scheduleCoreDownload($db); + $this->consumer('register'); } catch (\Throwable $e) { Factory::getApplication()->enqueueMessage( 'plg_task_cwmscripture: postflight failed — ' . $e->getMessage(), diff --git a/script.php b/script.php index 97493c6..9f0f74d 100644 --- a/script.php +++ b/script.php @@ -57,6 +57,9 @@ public function postflight(string $type, InstallerAdapter $adapter): bool // Silent — plugin row may not exist yet during discover install. } + // Declare our dependency on lib_cwmscripture + $this->consumer('register'); + return true; } @@ -77,6 +80,44 @@ public function update(InstallerAdapter $adapter): bool public function uninstall(InstallerAdapter $adapter): bool { + $this->consumer('unregister'); + return true; } + + /** + * Declare, or withdraw, this plugin's dependency on lib_cwmscripture. + * + * Joomla tracks no dependencies between extensions, so the library cannot + * discover who relies on it. Registering means the library refuses to be + * uninstalled while we are installed, and the shared #__bsms_bible_* tables + * are not dropped while we still read them. + * + * The library's own entry point handles autoloading, version tolerance and + * error handling, so this stays two lines. Note the group is part of the + * registry key — a plugin registered without it never matches on lookup. + * + * @param string $action 'register' on install/update, 'unregister' on removal + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + private function consumer(string $action): void + { + if (!is_file($helper = JPATH_LIBRARIES . '/cwmscripture/src/consumer.php')) { + return; + } + + $registry = require $helper; + + // Not one dynamic call: unregister() takes no display name. + if ($action === 'register') { + $registry->register('scripturelinks', 'plugin', 'content', 'Scripture Links (plg_content_scripturelinks)'); + + return; + } + + $registry->unregister('scripturelinks', 'plugin', 'content'); + } }; diff --git a/tests/unit/System/CwmscriptureSystemPluginTest.php b/tests/unit/System/CwmscriptureSystemPluginTest.php new file mode 100644 index 0000000..b51054c --- /dev/null +++ b/tests/unit/System/CwmscriptureSystemPluginTest.php @@ -0,0 +1,117 @@ +assertTrue( + Cwmscripture::sqlIsArmed(self::ARMED_1_1_4), + 'The v1.1.4 file has three executable DROP TABLE statements and must be disarmed.' + ); + } + + public function testDisarmedFileMentioningDropTableInCommentsIsNotArmed(): void + { + $this->assertFalse( + Cwmscripture::sqlIsArmed(self::DISARMED_WITH_PROSE), + 'The 1.1.5/1.1.6 files describe DROP TABLE in prose. Treating that as armed ' + . 'would rewrite a file that is already safe.' + ); + } + + public function testEmptyAndCommentOnlyFilesAreNotArmed(): void + { + $this->assertFalse(Cwmscripture::sqlIsArmed('')); + $this->assertFalse(Cwmscripture::sqlIsArmed("-- nothing to see here\n")); + } + + public function testUnrelatedStatementsAreNotArmed(): void + { + $this->assertFalse( + Cwmscripture::sqlIsArmed("DELETE FROM `#__bsms_scripture_cache`;\n"), + 'Only DROP TABLE is the hazard — other statements must not trigger a rewrite.' + ); + } + + public function testIndentedAndLowercaseStatementsAreArmed(): void + { + $this->assertTrue( + Cwmscripture::sqlIsArmed(" drop table if exists `#__bsms_bible_verses`;\n"), + 'Detection must not depend on case or leading whitespace.' + ); + } + + public function testStatementAfterCommentBlockIsArmed(): void + { + $sql = self::DISARMED_WITH_PROSE . "\nDROP TABLE IF EXISTS `#__bsms_bible_verses`;\n"; + + $this->assertTrue( + Cwmscripture::sqlIsArmed($sql), + 'A live statement appended below the reassuring comment block must still be caught.' + ); + } + + public function testCarriageReturnLineEndingsAreHandled(): void + { + $sql = "--\r\n-- header\r\n--\r\nDROP TABLE IF EXISTS `#__bsms_bible_verses`;\r\n"; + + $this->assertTrue( + Cwmscripture::sqlIsArmed($sql), + 'A file saved with CRLF endings is just as dangerous.' + ); + } +}