Skip to content

Commit 151756e

Browse files
fix(modules): prevent module deletion from wiping entire database
Bumps version to 0.31.10.0. Critical: - ModuleInstaller::rollbackModuleMigrations() called MigrationRunner::regress(0), which ignores the namespace filter in CI4 (regress() nulls $this->namespace internally and getBatches()/getBatchHistory() do not filter by namespace). Any single-module uninstall therefore downed every registered module's migrations and dropped the entire database. Replaced with a namespace-scoped getHistory() walk that calls force() per migration in reverse order; finally{} resets the shared runner's namespace. Sitemap: - BlogModel/PagesModel sitemapItems(): return path-only loc values. ci4seopro's SitemapBuilder prepends baseUrl, so site_url() produced duplicate scheme+host inside <loc> entries. - Both models now LEFT JOIN their *_langs tables so localized records are not filtered out. CSRF (backend): - setCsrfHash() in be-assets/js/ci4ms.js also rewrites every csrf_field() hidden input after a token refresh, so non-AJAX form submits following an AJAX call no longer hit 403. - ajaxPrefilter sends a pre-encoded URL string for empty POST bodies instead of building an object jQuery would later re-serialize away. Frontend: - templates/default/assets/ci4ms.js gates captchaF() on the presence of .captcha elements, removing the per-page-load /commentCaptcha POST. - funcs_helper.php switches the captcha refresh button from inline onclick="captchaF()" to a .captcha-refresh class bound via the existing delegated handler. Backend UX: - modules/Methods/Views/update.php: fix broken route_to('list') → route_to('methodList'); cast inNavigation/isBackoffice/hasChild to bool so checkboxes render active for stored 1/0 values. elFinder: - pageImgelfinderDialog / pageMultipleImgelfinderDialog now cache their jQuery wrapper and reopen the same instance instead of rebuilding on every call. sync interval exec() is wrapped in try/catch so a destroyed instance no longer throws every second. Misc: - .gitignore: exclude CLAUDE.md and ci4ms-specs/. - Install.php / Ci4msSetup.php: app.version → 0.31.10.0.
1 parent e1aad28 commit 151756e

11 files changed

Lines changed: 172 additions & 129 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ dist/
146146
composer.lock.json
147147
.antigravityignore
148148
.claude
149+
CLAUDE.md
150+
ci4ms-specs
149151
tests/*
150152
phpunit.xml.dist
151153
node_modules

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) conventions adapted to the existing four-component version numbers.
66

7+
## [0.31.10.0] - 2026-05-23
8+
9+
### Fixed
10+
11+
- **CRITICAL — Single Module Deletion Wiped Entire Database:** `ModuleInstaller::rollbackModuleMigrations()` called `MigrationRunner::regress(0)` under the assumption that `setNamespace()` scoped the rollback to a single module. CI4's `regress()` ignores the namespace filter (it nulls `$this->namespace` internally, and `getBatches()`/`getBatchHistory()` do not filter by namespace), so any uninstall would down every registered module's migrations and drop the entire database. Replaced with a namespace-filtered `getHistory()` walk that calls `force()` per migration, guaranteeing only the target module's tables are dropped and only its `migrations` table rows are removed. Added a `finally` block to reset the shared MigrationRunner singleton's namespace.
12+
- **Sitemap URL Duplication:** `BlogModel::sitemapItems()` and `PagesModel::sitemapItems()` returned fully-qualified URLs via `site_url()`, but `ci4seopro\Libraries\Seo\Search\SitemapBuilder` prepends `Seo::$baseUrl` to every `loc`, producing malformed `<loc>https://host.tldhttps://host.tld/...</loc>` entries. Models now return paths only (`'/' . ltrim($seflink, '/')`), matching the package contract.
13+
- **Sitemap Multilingual Coverage:** Both sitemap models now `LEFT JOIN` their `*_langs` tables so localized records are included instead of being filtered out by the primary-table-only query.
14+
- **Backend CSRF Hidden Input Stale After AJAX:** `setCsrfHash()` in `public/be-assets/js/ci4ms.js` only updated the `<meta name="X-CSRF-TOKEN">` tag after token regeneration, leaving every `csrf_field()` hidden input in the page bound to the previous token. Classic (non-AJAX) form submissions following an AJAX request received `403 Forbidden`. The setter now also writes the new hash to every `input[name="csrf_token_ci4ms"]` on the page.
15+
- **Backend CSRF Empty-Body POST Token Loss:** When an AJAX POST was sent with `data: null` or no `data` at all, `ajaxPrefilter` created a fresh object and assigned the token to it, but jQuery later re-serialized that object into an empty body, stripping the CSRF parameter. The prefilter now writes a pre-encoded URL-encoded string instead, so the token survives to the wire.
16+
- **Frontend Captcha Auto-Fire on All Pages:** `public/templates/default/assets/ci4ms.js` invoked `captchaF()` at file scope, firing a `POST /commentCaptcha` request on every public-side page load regardless of whether a captcha image was rendered. Wrapped the call in a DOM-ready guard that runs only when `.captcha` elements exist.
17+
- **Methods Update View Broken Route:** "Back to list" link in `modules/Methods/Views/update.php` referenced the non-existent route alias `list`; updated to the correct `methodList` alias.
18+
- **Methods Update View Checkbox Active State:** `inNavigation`, `isBackoffice`, and `hasChild` flags are stored as integers (`1` / `0`) but the view used strict `=== true` comparison, never marking checkboxes as active for existing records. Added `(bool)` casts so the `active` class and `checked` attribute apply correctly.
19+
20+
### Changed
21+
22+
- **elFinder Dialog Reuse:** `pageImgelfinderDialog()` and `pageMultipleImgelfinderDialog()` no longer create a fresh dialog on every call. The first invocation builds the dialog and caches the jQuery wrapper in a module-scoped variable (or keyed map for the multi-image variant); subsequent invocations call `dialogelfinder("open")` on the existing instance. Removes the `destroyOnClose: true` flag on these variants, prevents leaked event handlers, and stops repeated `cssAutoLoad` HTTP requests. The `sync` interval is wrapped in `try/catch` so a destroyed instance no longer throws a polling exception every second.
23+
- **Captcha Refresh Button Wiring:** The "New Captcha" button in `app/Helpers/templates/default/funcs_helper.php` switched from inline `onclick="captchaF()"` to a `.captcha-refresh` class that the existing jQuery delegation in `captchaF()` already binds. Cleaner separation between markup and behavior.
24+
25+
### Added
26+
27+
- **`.gitignore` Entries:** Excluded `CLAUDE.md` and `ci4ms-specs/` so per-developer agent tooling artifacts stay out of version control.
28+
729
## [0.31.9.0] - 2026-05-08
830

931
### Security
@@ -314,6 +336,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
314336

315337
- Expanded database migrations and introduced new supporting libraries.
316338

339+
[0.31.10.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.10.0
317340
[0.31.9.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.9.0
318341
[0.31.8.1]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.8.1
319342
[0.31.8.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.8.0

app/Helpers/templates/default/funcs_helper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function comments(array $comments, string $blog_id)
3131
<div class="input-group">
3232
<img src="" class="captcha" alt="captcha">
3333
<input type="text" placeholder="captcha" name="captcha" class="form-control">
34-
<button class="btn btn-secondary" onclick="captchaF()" type="button">New Captcha</button>
34+
<button class="btn btn-secondary captcha-refresh" type="button">New Captcha</button>
3535
</div>
3636
</div>
3737
<div class="col-6 form-group text-end">

app/Models/BlogModel.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ class BlogModel extends Model
1111

1212
public static function sitemapItems(): array
1313
{
14-
$blogs = model(self::class)->where(['isActive' => true, 'inXML' => true])->findAll();
14+
$blogs = model(self::class)->join('blog_langs','blog_langs.id = blog.id','left')->where(['isActive' => true, 'inXML' => true])->findAll();
1515
$items = [];
1616

1717
foreach ($blogs as $blog) {
1818
$items[] = [
19-
'loc' => site_url('blog/' . $blog['seflink']),
19+
'loc' => '/blog/' . ltrim($blog['seflink'], '/'),
2020
'lastmod' => $blog['updated_at'] ?? $blog['created_at'],
2121
'changefreq' => 'weekly',
2222
'priority' => 1.0,

app/Models/PagesModel.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ class PagesModel extends Model
1111

1212
public static function sitemapItems(): array
1313
{
14-
$pages = model(self::class)->where(['isActive' => true])->orderBy('seflink ASC')->findAll();
14+
$pages = model(self::class)->join('pages_langs','pages_langs.id = pages.id','left')->where(['isActive' => true])->orderBy('seflink ASC')->findAll();
1515
$items = [];
1616

1717
foreach ($pages as $page) {
1818
$items[] = [
19-
'loc' => site_url($page['seflink']),
19+
'loc' => '/' . ltrim($page['seflink'], '/'),
2020
'lastmod' => $page['updated_at'] ?? $page['creationDate'],
2121
'changefreq' => $page['changefreq'] ?? 'weekly',
2222
'priority' => $page['priority'] ?? 0.8,

modules/Backend/Commands/Ci4msSetup.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public function run(array $params)
220220
'app.supportedLocales' => '["ar","de","en","es","fr","hi","ja","pt","ru","tr","zh"]',
221221
'app.negotiateLocale' => 'true',
222222
'app.appTimezone' => '\'Europe/Istanbul\'',
223-
'app.version' => '0.31.9.0',
223+
'app.version' => '0.31.10.0',
224224
];
225225

226226
if (!$this->updateEnvSettings($updates)) {

modules/Install/Controllers/Install.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public function index()
6767
'app.supportedLocales' => '["ar","de","en","es","fr","hi","ja","pt","ru","tr","zh"]',
6868
'app.negotiateLocale' => 'true',
6969
'app.appTimezone' => '\'Europe/Istanbul\'',
70-
'app.version' => '0.31.9.0'
70+
'app.version' => '0.31.10.0'
7171
];
7272
if ($this->copyEnvFile() && $this->updateEnvSettings($updates)) $this->generateEncryptionKey();
7373

modules/Methods/Libraries/ModuleInstaller.php

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public function runAllPendingMigrations(): array
9090
public function runModuleSeeder(string $moduleName): array
9191
{
9292
$seederName = "Modules\\{$moduleName}\\Database\\Seeds\\{$moduleName}Seeder";
93-
93+
9494
// If such a Seeder class exists, run it
9595
if (class_exists($seederName)) {
9696
try {
@@ -182,21 +182,30 @@ public function rollbackModuleMigrations(string $moduleName): array
182182
return ['success' => true, 'rolledBack' => 0, 'error' => null];
183183
}
184184

185-
try {
186-
/** @var \CodeIgniter\Database\MigrationRunner $migrate */
187-
$migrate = Services::migrations();
188-
$namespace = 'Modules\\' . $moduleName . '\\Database\\Migrations';
189-
190-
// Roll back all migrations (batch 0 = roll back all)
191-
$migrate->setNamespace($namespace)->regress(0);
185+
/** @var \CodeIgniter\Database\MigrationRunner $migrate */
186+
$migrate = Services::migrations();
187+
$namespace = 'Modules\\' . $moduleName . '\\Database\\Migrations';
192188

193-
$files = glob($migrationPath . '/*.php');
194-
$rolledBack = is_array($files) ? count($files) : 0;
189+
try {
190+
$migrate->setNamespace($namespace);
191+
$dbGroup = (new \Config\Database())->defaultGroup;
192+
$history = $migrate->getHistory($dbGroup);
193+
194+
$rolledBack = 0;
195+
foreach (array_reverse($history) as $row) {
196+
$file = $migrationPath . '/' . $row->version . '_' . $row->class . '.php';
197+
if (is_file($file)) {
198+
$migrate->force($file, $namespace);
199+
$rolledBack++;
200+
}
201+
}
195202

196203
return ['success' => true, 'rolledBack' => $rolledBack, 'error' => null];
197204
} catch (\Throwable $e) {
198205
log_message('error', "[ModuleInstaller] Rollback failed for {$moduleName}: {$e->getMessage()}");
199206
return ['success' => false, 'rolledBack' => 0, 'error' => $e->getMessage()];
207+
} finally {
208+
$migrate->setNamespace(null);
200209
}
201210
}
202211

modules/Methods/Views/update.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
</div>
1616
<div class="col-sm-6">
1717
<ol class="breadcrumb float-sm-right">
18-
<a href="<?php echo route_to('list') ?>" class="btn btn-sm btn-outline-info"><?php echo lang('Backend.backToList') ?></a>
18+
<a href="<?php echo route_to('methodList') ?>" class="btn btn-sm btn-outline-info"><?php echo lang('Backend.backToList') ?></a>
1919
</ol>
2020
</div>
2121
</div>
@@ -101,14 +101,14 @@
101101
</div>
102102
<div class="form-group col-md-9 d-flex align-items-end">
103103
<div class="w-100 btn-group btn-group-toggle" data-toggle="buttons">
104-
<label class="btn btn-outline-primary <?php echo $method->inNavigation === true ? 'active' : '' ?>">
105-
<input class="custom-control-input" value="1" name="inNavigation" type="checkbox" id="inNavigation" <?php echo $method->inNavigation === true ? 'checked' : '' ?>> Menüde mi ?
104+
<label class="btn btn-outline-primary <?php echo (bool)$method->inNavigation === true ? 'active' : '' ?>">
105+
<input class="custom-control-input" value="1" name="inNavigation" type="checkbox" id="inNavigation" <?php echo (bool)$method->inNavigation === true ? 'checked' : '' ?>> Menüde mi ?
106106
</label>
107-
<label class="btn btn-outline-primary <?php echo $method->isBackoffice === true ? 'active' : '' ?>">
108-
<input class="custom-control-input" value="1" name="isBackoffice" type="checkbox" id="isBackoffice" <?php echo $method->isBackoffice === true ? 'checked' : '' ?>> Panelde mi ?
107+
<label class="btn btn-outline-primary <?php echo (bool)$method->isBackoffice === true ? 'active' : '' ?>">
108+
<input class="custom-control-input" value="1" name="isBackoffice" type="checkbox" id="isBackoffice" <?php echo (bool)$method->isBackoffice === true ? 'checked' : '' ?>> Panelde mi ?
109109
</label>
110110
<label class="btn btn-outline-primary <?php echo $method->hasChild === true ? 'active' : '' ?>">
111-
<input class="custom-control-input" value="1" name="hasChild" type="checkbox" id="hasChild" <?php echo $method->hasChild === true ? 'checked' : '' ?>> Alt sayfası var mı ?
111+
<input class="custom-control-input" value="1" name="hasChild" type="checkbox" id="hasChild" <?php echo (bool)$method->hasChild === true ? 'checked' : '' ?>> Alt sayfası var mı ?
112112
</label>
113113
</div>
114114
</div>

0 commit comments

Comments
 (0)