Skip to content

Commit fca1af2

Browse files
bcordisclaude
andauthored
feat: migrate scripture settings to ScriptureLinks plugin (#1191)
* feat: migrate scripture settings from Proclaim to ScriptureLinks plugin Move scripture provider settings (GetBible, API.Bible, API key, GDPR mode, cache days, default version) from component params to plugin params. The scripture system becomes self-contained in the library + plugin. Library changes: - ScriptureParamsHelper: read/write plugin params from #__extensions - ApiKeyField + BibleTranslationField: moved to library namespace - TranslationsmanagerField: full Scripture tab UI (panels + table) - bible-translations.js + cwm-fetch.js: copied to library media Plugin changes: - onAjaxScripturelinks: 9 AJAX actions for Proclaim + 3 for plugin UI - Settings tab: mode + display only - Scripture tab: full provider/settings/translations management Proclaim changes: - Admin Center Scripture tab reads/writes plugin params - CwmadminModel intercepts save to redirect scripture params - bible-translations.js AJAX URLs use com_ajax - Controller: removed ~630 lines of scripture AJAX methods - Form fields: thin wrappers delegating to library - Migration script copies params on upgrade Build changes: - Library manifest symlinked for namespace autoloading - npm build outputs updated JS with com_ajax URLs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update submodule — scripture tab detection for plugin page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update submodule — field order, switcher styling, GDPR on plugin tab Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update submodule — element IDs and HttpFactory for plugin page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: Proclaim admin uses shared Scripture tab renderer Replace ~130 lines of custom Scripture tab HTML + 60-line config div with a single call to TranslationsmanagerField::renderScriptureTab(). Both plugin and Proclaim now render identical Scripture tab UI from the same code path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update submodule — API key field show/hide on provider toggle Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: update submodule — GDPR Mode on plugin tab only Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: bidirectional GDPR mode sync between Proclaim and plugin gdpr_mode is a shared setting — Proclaim needs it for analytics/privacy, the plugin needs it for scripture API calls. - Proclaim keeps gdpr_mode in component params (not stripped on save) - Proclaim syncs gdpr_mode to plugin params via SHARED_KEYS - Plugin syncs gdpr_mode back to #__bsms_admin.params on save - CwmanalyticsHelper reads from component params (its own copy) - Migration copies gdpr_mode to plugin but keeps component copy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: update submodule — remove BibleGateway external links All scripture display modes now use local/API text only. No external links that expose visitor IPs to third parties. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: update submodule — attribute-style tag parameters Supports {scripture display="tooltip" version="kjv"}...{/scripture} per-tag overrides for display mode and translation version. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f11ecf1 commit fca1af2

File tree

12 files changed

+334
-1098
lines changed

12 files changed

+334
-1098
lines changed

admin/src/Controller/CwmadminController.php

Lines changed: 4 additions & 644 deletions
Large diffs are not rendered by default.

admin/src/Field/ApiKeyField.php

Lines changed: 5 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -7,97 +7,22 @@
77
* @copyright (C) 2026 CWM Team All rights reserved
88
* @license GNU General Public License version 2 or later; see LICENSE.txt
99
* @link https://www.christianwebministries.org
10-
* */
10+
*/
1111

1212
namespace CWM\Component\Proclaim\Administrator\Field;
1313

1414
// phpcs:disable PSR1.Files.SideEffects
1515
\defined('_JEXEC') or die;
16-
1716
// phpcs:enable PSR1.Files.SideEffects
1817

19-
use Joomla\CMS\Form\Field\TextField;
18+
use CWM\Library\Scripture\Field\ApiKeyField as LibraryApiKeyField;
2019

2120
/**
22-
* API Key Field - Masked text input with reveal toggle
23-
*
24-
* Renders as a password field with an eye toggle button.
25-
* When a value exists, shows dots with the last 4 characters as a placeholder hint.
21+
* Backward-compatible wrapper — delegates to the library field.
2622
*
27-
* @package Proclaim.Admin
2823
* @since 10.1.0
24+
* @deprecated 10.3.0 Use CWM\Library\Scripture\Field\ApiKeyField directly.
2925
*/
30-
class ApiKeyField extends TextField
26+
class ApiKeyField extends LibraryApiKeyField
3127
{
32-
/**
33-
* The form field type.
34-
*
35-
* @var string
36-
* @since 10.1.0
37-
*/
38-
protected $type = 'ApiKey';
39-
40-
/**
41-
* Get the field input markup.
42-
*
43-
* @return string The field input markup.
44-
*
45-
* @since 10.1.0
46-
*/
47-
protected function getInput(): string
48-
{
49-
$value = (string) $this->value;
50-
51-
// If no value, render a normal text input
52-
if ($value === '') {
53-
return parent::getInput();
54-
}
55-
56-
$name = $this->name;
57-
$id = $this->id;
58-
$class = $this->class ? ' ' . $this->class : '';
59-
$disabled = $this->disabled ? ' disabled' : '';
60-
$readonly = $this->readonly ? ' readonly' : '';
61-
$required = $this->required ? ' required' : '';
62-
$hint = $this->hint ? ' placeholder="' . htmlspecialchars($this->hint, ENT_COMPAT, 'UTF-8') . '"' : '';
63-
$autocomplete = $this->autocomplete ?? '';
64-
$acAttr = $autocomplete !== '' ? ' autocomplete="' . htmlspecialchars($autocomplete, ENT_COMPAT, 'UTF-8') . '"' : '';
65-
66-
// Build last-4-char hint for placeholder
67-
$lastFour = substr($value, -4);
68-
$maskedHint = str_repeat("\u{2022}", 20) . $lastFour;
69-
70-
$toggleId = $id . '_toggle';
71-
72-
$html = '<div class="input-group">';
73-
$html .= '<input type="password"'
74-
. ' name="' . $name . '"'
75-
. ' id="' . $id . '"'
76-
. ' value="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"'
77-
. ' placeholder="' . htmlspecialchars($maskedHint, ENT_COMPAT, 'UTF-8') . '"'
78-
. ' class="form-control' . $class . '"'
79-
. $disabled . $readonly . $required . $acAttr
80-
. ' />';
81-
$html .= '<button type="button" class="btn btn-secondary" id="' . $toggleId . '"'
82-
. ' aria-label="Toggle visibility">';
83-
$html .= '<span class="icon-eye" aria-hidden="true"></span>';
84-
$html .= '</button>';
85-
$html .= '</div>';
86-
87-
$html .= '<script>'
88-
. 'document.getElementById(' . json_encode($toggleId) . ').addEventListener("click", function() {'
89-
. ' var input = document.getElementById(' . json_encode($id) . ');'
90-
. ' var icon = this.querySelector("span");'
91-
. ' if (input.type === "password") {'
92-
. ' input.type = "text";'
93-
. ' icon.className = "icon-eye-close";'
94-
. ' } else {'
95-
. ' input.type = "password";'
96-
. ' icon.className = "icon-eye";'
97-
. ' }'
98-
. '});'
99-
. '</script>';
100-
101-
return $html;
102-
}
10328
}

admin/src/Field/BibleTranslationField.php

Lines changed: 6 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -11,205 +11,18 @@
1111

1212
namespace CWM\Component\Proclaim\Administrator\Field;
1313

14-
use CWM\Component\Proclaim\Administrator\Helper\Cwmparams;
15-
use Joomla\CMS\Factory;
16-
use Joomla\CMS\Form\Field\GroupedlistField;
17-
use Joomla\Database\DatabaseInterface;
18-
use Joomla\Registry\Registry;
19-
2014
// phpcs:disable PSR1.Files.SideEffects
2115
\defined('_JEXEC') or die;
2216
// phpcs:enable PSR1.Files.SideEffects
2317

18+
use CWM\Library\Scripture\Field\BibleTranslationField as LibraryBibleTranslationField;
19+
2420
/**
25-
* Bible Translation selection field with language-grouped optgroups.
26-
*
27-
* Shows translations from #__bsms_bible_translations grouped by language.
28-
* Uses Joomla's GroupedlistField to render `<optgroup>` elements, and
29-
* supports Choices.js search via `layout="joomla.form.field.list-fancy-select"`.
30-
*
31-
* When the XML attribute `servable_only="true"` is set, only translations
32-
* that can actually be served are shown: locally installed translations
33-
* plus translations from enabled online providers.
21+
* Backward-compatible wrapper — delegates to the library field.
3422
*
35-
* @since 10.1.0
23+
* @since 10.1.0
24+
* @deprecated 10.3.0 Use CWM\Library\Scripture\Field\BibleTranslationField directly.
3625
*/
37-
class BibleTranslationField extends GroupedlistField
26+
class BibleTranslationField extends LibraryBibleTranslationField
3827
{
39-
/**
40-
* The field type.
41-
*
42-
* @var string
43-
* @since 10.1.0
44-
*/
45-
protected $type = 'BibleTranslation';
46-
47-
/**
48-
* ISO language code to human-readable name map.
49-
*
50-
* @var array<string, string>
51-
* @since 10.1.0
52-
*/
53-
private const LANGUAGE_NAMES = [
54-
'af' => 'Afrikaans', 'am' => 'Amharic', 'ar' => 'Arabic',
55-
'bg' => 'Bulgarian', 'bn' => 'Bengali', 'cs' => 'Czech',
56-
'da' => 'Danish', 'de' => 'German', 'el' => 'Greek',
57-
'en' => 'English', 'eo' => 'Esperanto', 'es' => 'Spanish',
58-
'et' => 'Estonian', 'fa' => 'Persian', 'fi' => 'Finnish',
59-
'fr' => 'French', 'ga' => 'Irish', 'he' => 'Hebrew',
60-
'hi' => 'Hindi', 'hr' => 'Croatian', 'hu' => 'Hungarian',
61-
'id' => 'Indonesian', 'is' => 'Icelandic', 'it' => 'Italian',
62-
'ja' => 'Japanese', 'ko' => 'Korean', 'la' => 'Latin',
63-
'lt' => 'Lithuanian', 'lv' => 'Latvian', 'mk' => 'Macedonian',
64-
'ml' => 'Malayalam', 'mr' => 'Marathi', 'ms' => 'Malay',
65-
'my' => 'Burmese', 'ne' => 'Nepali', 'nl' => 'Dutch',
66-
'no' => 'Norwegian', 'pl' => 'Polish', 'pt' => 'Portuguese',
67-
'ro' => 'Romanian', 'ru' => 'Russian', 'sk' => 'Slovak',
68-
'sl' => 'Slovenian', 'sq' => 'Albanian', 'sr' => 'Serbian',
69-
'sv' => 'Swedish', 'sw' => 'Swahili', 'ta' => 'Tamil',
70-
'te' => 'Telugu', 'th' => 'Thai', 'tl' => 'Tagalog',
71-
'tr' => 'Turkish', 'uk' => 'Ukrainian', 'ur' => 'Urdu',
72-
'vi' => 'Vietnamese', 'zh' => 'Chinese',
73-
];
74-
75-
/**
76-
* Get the field option groups.
77-
*
78-
* Returns translations grouped by language as `<optgroup>` sections.
79-
* The admin's current language group is sorted first.
80-
*
81-
* @return array Associative array of group label => option arrays
82-
*
83-
* @since 10.1.0
84-
*/
85-
/**
86-
* Get the field option groups.
87-
*
88-
* Returns translations grouped by language as `<optgroup>` sections.
89-
* The admin's current language group is sorted first.
90-
*
91-
* @return array Associative array of group label => option arrays
92-
*
93-
* @since 10.1.0
94-
*/
95-
protected function getGroups(): array
96-
{
97-
$groups = [];
98-
$servableOnly = ((string) ($this->element['servable_only'] ?? '')) === 'true';
99-
100-
// Determine which online provider sources are enabled
101-
$enabledSources = [];
102-
103-
if ($servableOnly) {
104-
try {
105-
$admin = Cwmparams::getAdmin();
106-
$adminParams = $admin->params ?? new Registry();
107-
} catch (\Exception $e) {
108-
$adminParams = new Registry();
109-
}
110-
111-
$gdprMode = (int) $adminParams->get('gdpr_mode', 0) === 1;
112-
113-
if (!$gdprMode && (int) $adminParams->get('provider_getbible', 1) === 1) {
114-
$enabledSources[] = 'getbible';
115-
}
116-
117-
if (!$gdprMode && (int) $adminParams->get('provider_api_bible', 0) === 1
118-
&& !empty($adminParams->get('api_bible_api_key', ''))) {
119-
$enabledSources[] = 'api_bible';
120-
}
121-
}
122-
123-
// Detect admin language for sorting priority
124-
$currentLang = 'en';
125-
126-
try {
127-
$currentLang = substr(Factory::getApplication()->getLanguage()->getTag(), 0, 2);
128-
} catch (\Exception $e) {
129-
// Default to English
130-
}
131-
132-
// Load translations from database grouped by language
133-
$byLanguage = [];
134-
135-
try {
136-
$db = Factory::getContainer()->get(DatabaseInterface::class);
137-
$query = $db->getQuery(true)
138-
->select($db->quoteName(['abbreviation', 'name', 'language', 'installed', 'source']))
139-
->from($db->quoteName('#__bsms_bible_translations'))
140-
->order($db->quoteName('language') . ' ASC, ' . $db->quoteName('name') . ' ASC');
141-
$db->setQuery($query);
142-
$translations = $db->loadObjectList();
143-
144-
if (!empty($translations)) {
145-
foreach ($translations as $row) {
146-
$installed = (int) $row->installed === 1;
147-
148-
// In servable_only mode, skip translations that can't be served
149-
if ($servableOnly && !$installed && !\in_array($row->source, $enabledSources, true)) {
150-
continue;
151-
}
152-
153-
$langCode = substr($row->language ?? '', 0, 2);
154-
$langName = self::LANGUAGE_NAMES[$langCode] ?? ucfirst($langCode ?: 'Other');
155-
156-
$label = $row->name . ' (' . strtoupper($row->abbreviation) . ')';
157-
158-
if ($installed) {
159-
$label .= '';
160-
}
161-
162-
$byLanguage[$langName][] = (object) [
163-
'value' => $row->abbreviation,
164-
'text' => $label,
165-
];
166-
}
167-
}
168-
} catch (\Exception $e) {
169-
// Database table might not exist yet during install
170-
}
171-
172-
// If no translations found, provide common defaults
173-
if (empty($byLanguage)) {
174-
$defaults = [
175-
'kjv' => 'King James Version',
176-
'web' => 'World English Bible',
177-
'asv' => 'American Standard Version',
178-
'ylt' => "Young's Literal Translation",
179-
];
180-
181-
foreach ($defaults as $abbr => $name) {
182-
$byLanguage['English'][] = (object) [
183-
'value' => $abbr,
184-
'text' => $name . ' (' . strtoupper($abbr) . ')',
185-
];
186-
}
187-
}
188-
189-
// Sort language groups: admin language first, then alphabetical
190-
uksort($byLanguage, function (string $a, string $b) use ($currentLang): int {
191-
$aLang = array_search($a, self::LANGUAGE_NAMES, true) ?: '';
192-
$bLang = array_search($b, self::LANGUAGE_NAMES, true) ?: '';
193-
$aIsNative = $aLang === $currentLang;
194-
$bIsNative = $bLang === $currentLang;
195-
196-
if ($aIsNative && !$bIsNative) {
197-
return -1;
198-
}
199-
200-
if (!$aIsNative && $bIsNative) {
201-
return 1;
202-
}
203-
204-
return strcasecmp($a, $b);
205-
});
206-
207-
// Build groups array for GroupedlistField
208-
foreach ($byLanguage as $langName => $options) {
209-
$groups[$langName] = $options;
210-
}
211-
212-
// Merge with any parent groups (adds "- Select -" placeholder from XML)
213-
return array_merge(parent::getGroups(), $groups);
214-
}
21528
}

admin/src/Field/BibleVersionField.php

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace CWM\Component\Proclaim\Administrator\Field;
1313

14-
use CWM\Component\Proclaim\Administrator\Helper\Cwmparams;
14+
use CWM\Library\Scripture\Helper\ScriptureParamsHelper;
1515
use Joomla\CMS\Factory;
1616
use Joomla\CMS\Form\Field\ListField;
1717
use Joomla\Database\DatabaseInterface;
@@ -113,23 +113,18 @@ public function setup(\SimpleXMLElement $element, $value, $group = null): bool
113113
{
114114
$result = parent::setup($element, $value, $group);
115115

116-
// If no value is set (new record), use the admin default
116+
// If no value is set (new record), use the plugin default
117117
if ($result && ($this->value === null || $this->value === '')) {
118118
$default = 'kjv';
119119

120120
try {
121-
$admin = Cwmparams::getAdmin();
122-
$params = $admin->params ?? null;
121+
$pluginDefault = ScriptureParamsHelper::getParams()->get('default_version', '');
123122

124-
if ($params) {
125-
$adminDefault = $params->get('default_bible_version', '');
126-
127-
if (!empty($adminDefault)) {
128-
$default = $adminDefault;
129-
}
123+
if (!empty($pluginDefault)) {
124+
$default = $pluginDefault;
130125
}
131126
} catch (\Exception $e) {
132-
// Ignore — no admin params available, use 'kjv' fallback
127+
// Ignore — plugin params unavailable, use 'kjv' fallback
133128
}
134129

135130
$this->value = $default;
@@ -172,20 +167,19 @@ protected function getOptions(): array
172167

173168
if ($servableOnly) {
174169
try {
175-
$admin = Cwmparams::getAdmin();
176-
$adminParams = $admin->params ?? new Registry();
170+
$pluginParams = ScriptureParamsHelper::getParams();
177171
} catch (\Exception $e) {
178-
$adminParams = new Registry();
172+
$pluginParams = new Registry();
179173
}
180174

181-
$gdprMode = (int) $adminParams->get('gdpr_mode', 0) === 1;
175+
$gdprMode = (int) $pluginParams->get('gdpr_mode', 0) === 1;
182176

183-
if (!$gdprMode && (int) $adminParams->get('provider_getbible', 1) === 1) {
177+
if (!$gdprMode && (int) $pluginParams->get('provider_getbible', 1) === 1) {
184178
$enabledSources[] = 'getbible';
185179
}
186180

187-
if (!$gdprMode && (int) $adminParams->get('provider_api_bible', 0) === 1
188-
&& !empty($adminParams->get('api_bible_api_key', ''))) {
181+
if (!$gdprMode && (int) $pluginParams->get('provider_api_bible', 0) === 1
182+
&& !empty($pluginParams->get('api_bible_api_key', ''))) {
189183
$enabledSources[] = 'api_bible';
190184
}
191185
}

admin/src/Helper/CwmanalyticsHelper.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ public static function isOptedOut(): bool
353353
return false;
354354
}
355355

356+
// GDPR mode — Proclaim keeps its own copy in component params
356357
if ($params->get('gdpr_mode', '0')) {
357358
return true;
358359
}

0 commit comments

Comments
 (0)