Skip to content

Commit bef25c4

Browse files
add inline status toggle, superadmin protection, i18n, and elFinder
Pages: - Add isActive() AJAX endpoint for inline page activation/deactivation - Auto-remove deactivated pages from navigation menu with cache flush - Show homepage badge in DataTables listing with real-time updates - Filter inactive pages from frontend rendering (Home controller) Users: - Prevent deletion of superadmin users with localized error message - Fix DataTables search (remove erroneous $like=[] reassignment) - Change user_del() to read ID from POST instead of URL segment - Add CSRF exemptions for AJAX user management endpoints Menu: - Replace all hardcoded Turkish strings with lang() calls (EN + TR) - Switch refreshLeftList() from POST to GET for idempotent reads Media: - Upgrade elFinder from 2.1.66 to 2.1.67 (JS, CSS, i18n, help files) - Bypass elFinder internal CSRF; CI4 Shield session auth suffices Sitemap: - Respect App.siteLanguageMode in Blog/Pages sitemap models - Add public/sitemap.css stylesheet for browser-readable sitemaps Backup: - Fix AJAX promise chain (done/fail/always) for create and delete - Defer DataTables reload after CSRF meta tag update Infrastructure: - Declare DataTable variable at module scope in Backup and Users views - Simplify template filter path resolution in Filters.php - Update documentation (architecture, handbook, README, CHANGELOG)
1 parent 1d27a87 commit bef25c4

76 files changed

Lines changed: 1632 additions & 879 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,36 @@ 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.32.0.0] - 2026-06-03
8+
9+
### Added
10+
11+
- **Pages: Inline Status Toggle:** New `isActive()` AJAX endpoint allows administrators to activate or deactivate pages directly from the DataTables listing without opening the edit form. Deactivating a page automatically removes it from the navigation menu and invalidates the corresponding per-locale menu cache.
12+
- **Pages: Homepage Badge:** The pages list now shows a visual "Home" badge next to the page currently set as the homepage, updating in real time when the homepage selection changes.
13+
- **Sitemap Stylesheet:** Added `public/sitemap.css` to style the XML sitemap with a clean, readable layout for browsers.
14+
- **Users: Superadmin Delete Protection:** `user_del()` now verifies the caller holds the `superadmin` role and prevents deletion of any user belonging to the `superadmin` group, returning a localized error message (`cannotDeleteSuperadmin`) instead.
15+
- **Menu Module: Full Internationalization:** All hardcoded Turkish strings in the Menu module views and JavaScript (stat labels, toast messages, confirm dialogs) have been replaced with `lang()` calls backed by new language keys in both English and Turkish language files.
16+
17+
### Changed
18+
19+
- **elFinder Upgrade to 2.1.67:** Updated all elFinder JS, CSS, and i18n files from 2.1.66 to 2.1.67. Added three new help files (`fr`, `zh_CN`, `zh_TW`). Asset `<script>` tags now include a `?v=2.1.67` cache-buster.
20+
- **elFinder CSRF Bypass:** elFinder's internal CSRF token validation has been disabled via an anonymous class override because CI4 Shield's session-based authentication and the `backendGuard` filter already protect the connector endpoint; the internal CSRF mechanism was causing stale-token 403 errors during multi-request file operations.
21+
- **Frontend: Inactive Pages Hidden:** `App\Controllers\Home` now adds an `isActive = 1` condition when resolving page content, preventing deactivated pages from being rendered on the public site.
22+
- **Sitemap: Single-Language Mode:** `BlogModel::sitemapItems()` and `PagesModel::sitemapItems()` now check `App.siteLanguageMode`; in single-language mode, only records matching the default locale are emitted, eliminating duplicate sitemap entries.
23+
- **Users: DataTables Search Fix:** Removed the erroneous `$like = []` reassignment in `UserController::index()` that was silently discarding the search string parsed from the DataTables request.
24+
- **Users: CSRF Exemptions:** Added `backend/users/removeFromBlacklist`, `backend/users/blackList`, `backend/users/forceResetPassword`, and `backend/users/user_del` to `UsersConfig::$csrfExcept` so AJAX-based user management actions no longer fail on token regeneration.
25+
- **Users: `user_del()` Signature Change:** The method no longer accepts a URL segment parameter; the target user ID is now read exclusively from POST data, matching the AJAX call pattern.
26+
- **Backup Module: AJAX Reliability:** Backup create and delete operations now use the `.done()/.fail()/.always()` promise chain instead of the legacy `$.post(url, data, callback, type)` signature. The DataTables reload is deferred via `setTimeout(0)` to ensure it runs after the CSRF meta tag update.
27+
- **Backup & Users Views: DataTable Scope Fix:** The DataTable instance variable in both Backup and Users list views is now declared at module scope (outside the `$(function(){})` wrapper) so that external functions (e.g. create/delete handlers) can call `table.ajax.reload()` without `ReferenceError`.
28+
- **Filters.php: Template Filter Path Simplification:** The active theme's filter directory is now resolved with a simple `APPPATH` concatenation instead of the `resolve_template_path()` helper, removing an unnecessary abstraction layer and null-check branch.
29+
- **Menu Module: `refreshLeftList()` changed from POST to GET:** The left sidebar panel refresh now uses `$.get()` instead of `$.post()`, matching the idempotent nature of the request and eliminating the need for CSRF token injection on a read-only call.
30+
- **Pages Controller: Code Style Normalization:** Minor formatting changes (alignment, brace style, cast spacing) applied across the Pages controller for consistency with the project's coding standards.
31+
32+
### Fixed
33+
34+
- **Users: Search Broken in DataTables:** The `$like` variable was overwritten with an empty array immediately after being parsed from the DataTables request, making search effectively non-functional. The erroneous reassignment has been removed.
35+
- **Pages: `setHomePage` Client-Side Stale Badge:** After toggling the homepage via AJAX, the JavaScript `homePageId` variable was not updated, causing the "Home" badge to appear on the wrong row until a full page reload. The variable is now updated immediately upon a successful response.
36+
737
## [0.31.11.0] - 2026-05-24
838

939
### Fixed
@@ -348,6 +378,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
348378

349379
- Expanded database migrations and introduced new supporting libraries.
350380

381+
[0.32.0.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.32.0.0
382+
[0.31.11.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.11.0
351383
[0.31.10.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.10.0
352384
[0.31.9.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.9.0
353385
[0.31.8.1]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.8.1

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ Standard CodeIgniter commands (`php spark db:seed`, `php spark key:generate`, et
172172

173173
## Developer Notes
174174

175-
- **Cache keys**: `settings` (24h), `menus` (24h), `{userId}_permissions`. Clear with `php spark cache:clear` or `cache()->delete()`.
175+
- **Cache keys**: `settings` (24h), `menus_{locale}` (per-locale, 24h), `{userId}_permissions`. Clear with `php spark cache:clear` or `cache()->delete()`.
176176
- **Base controller**: Extend `Modules\Backend\Controllers\BaseController` for new backend controllers; it prepares session user, navigation, mail settings, and shared data.
177177
- **Permissions**: Register new secured routes in `Modules\Methods` (or via the database) so the permission filter recognizes them.
178178
- **Slug generation**: `seflink()` handles transliteration (including Turkish characters).

app/Config/Filters.php

Lines changed: 61 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,22 @@ class Filters extends BaseFilters
2828
* or [filter_name => [classname1, classname2, ...]]
2929
*/
3030
public array $aliases = [
31-
'csrf' => CSRF::class,
32-
'toolbar' => DebugToolbar::class,
33-
'honeypot' => Honeypot::class,
34-
'invalidchars' => InvalidChars::class,
31+
'csrf' => CSRF::class,
32+
'toolbar' => DebugToolbar::class,
33+
'honeypot' => Honeypot::class,
34+
'invalidchars' => InvalidChars::class,
3535
'secureheaders' => SecureHeaders::class,
36-
'cors' => Cors::class,
37-
'forcehttps' => ForceHTTPS::class,
36+
'cors' => Cors::class,
37+
'forcehttps' => ForceHTTPS::class,
3838
//'pagecache' => PageCache::class,
3939
//'performance' => PerformanceMetrics::class,
40-
'session' => \CodeIgniter\Shield\Filters\SessionAuth::class,
41-
'tokens' => \CodeIgniter\Shield\Filters\TokenAuth::class,
42-
'hmac' => \CodeIgniter\Shield\Filters\HmacAuth::class,
43-
'chain' => \CodeIgniter\Shield\Filters\ChainAuth::class,
44-
'auth-rates' => \CodeIgniter\Shield\Filters\AuthRates::class,
45-
'group' => \CodeIgniter\Shield\Filters\GroupFilter::class,
46-
'permission' => \CodeIgniter\Shield\Filters\PermissionFilter::class,
40+
'session' => \CodeIgniter\Shield\Filters\SessionAuth::class,
41+
'tokens' => \CodeIgniter\Shield\Filters\TokenAuth::class,
42+
'hmac' => \CodeIgniter\Shield\Filters\HmacAuth::class,
43+
'chain' => \CodeIgniter\Shield\Filters\ChainAuth::class,
44+
'auth-rates' => \CodeIgniter\Shield\Filters\AuthRates::class,
45+
'group' => \CodeIgniter\Shield\Filters\GroupFilter::class,
46+
'permission' => \CodeIgniter\Shield\Filters\PermissionFilter::class,
4747
'force-reset' => \CodeIgniter\Shield\Filters\ForcePasswordResetFilter::class,
4848
//'jwt' => \CodeIgniter\Shield\Filters\JWTAuth::class,
4949
'seofilter' => SearchSeoFilter::class,
@@ -137,16 +137,17 @@ public function __construct()
137137
$formatRules = new \CodeIgniter\Validation\FormatRules();
138138
foreach ($this->settings as $setting) {
139139
if ($formatRules->valid_json($setting->value) === true)
140-
$set[$setting->key] = (object)json_decode($setting->value, JSON_UNESCAPED_UNICODE);
141-
else $set[$setting->key] = $setting->value;
140+
$set[$setting->key] = (object) json_decode($setting->value, JSON_UNESCAPED_UNICODE);
141+
else
142+
$set[$setting->key] = $setting->value;
142143
}
143144
cache()->save('settings', $set, 86400);
144-
$this->settings = (object)$set;
145+
$this->settings = (object) $set;
145146
} else {
146-
$this->settings = (object)cache('settings');
147+
$this->settings = (object) cache('settings');
147148
}
148149
} catch (\Throwable $e) {
149-
$this->settings = (object)[];
150+
$this->settings = (object) [];
150151
}
151152
}
152153

@@ -163,12 +164,8 @@ public function __construct()
163164
}
164165
}
165166

166-
if (!empty($this->settings) && isset($this->settings->templateInfos->path)) {
167-
$filtersDir = resolve_template_path(APPPATH . 'Filters/templates/', $this->settings->templateInfos->path);
168-
if ($filtersDir !== null) {
169-
$mods[] = $filtersDir;
170-
}
171-
}
167+
if (!empty($this->settings) && isset($this->settings->templateInfos) && isset($this->settings->templateInfos->path))
168+
$mods[] = APPPATH . 'Filters/templates/' . $this->settings->templateInfos->path;
172169

173170
$this->loadDynamicFilters($mods);
174171

@@ -188,6 +185,7 @@ private function loadDynamicFilters(array $directories): void
188185
\Modules\Auth\Filters\Ci4MsAuthFilter::class,
189186
\Modules\Backend\Filters\BackendLogFilter::class,
190187
\Modules\Auth\Filters\SessionTracker::class,
188+
\Modules\Backend\Filters\CsrfTokenRefreshFilter::class,
191189
];
192190
$this->aliases['langfilter'] = [
193191
\App\Filters\Ci4ms::class,
@@ -248,8 +246,8 @@ private function loadConfig(): void
248246
$allFilters = [];
249247
$allGlobals = [];
250248

251-
foreach ($modules as $module) {
252-
$configClass = "Modules\\{$module}\\Config\\{$module}Config";
249+
if (!empty($this->settings) && isset($this->settings->templateInfos) && isset($this->settings->templateInfos->path)) {
250+
$configClass = '\\Config\\templates\\' . $this->settings->templateInfos->path . '\\ThemeConfig';
253251
if (class_exists($configClass)) {
254252
$configInstance = new $configClass();
255253
if (property_exists($configInstance, 'csrfExcept') && is_array($configInstance->csrfExcept)) {
@@ -275,34 +273,49 @@ private function loadConfig(): void
275273
$allCsrfExcept = array_unique($allCsrfExcept);
276274
}
277275
}
278-
$this->filters = array_merge($this->filters, $allFilters);
279-
$this->mergeCsrfExcept($allCsrfExcept);
280-
if (file_exists(ROOTPATH . '.env') && !empty($this->commonModel)) {
281-
try {
282-
$themeSlug = $this->settings->templateInfos->path ?? null;
283-
$themeConfigDir = ($this->commonModel->db->tableExists('settings'))
284-
? resolve_template_path(APPPATH . 'Config/templates/', $themeSlug)
285-
: null;
286-
if ($themeConfigDir !== null) {
287-
$configFile = $themeConfigDir . '/' . ucfirst($themeSlug) . 'Config.php';
288-
if (is_file($configFile) && realpath($configFile) === $configFile) {
289-
$className = '\\Config\\templates\\' . $themeSlug . '\\' . ucfirst($themeSlug) . 'Config';
290-
if (class_exists($className)) {
291-
$themeConfig = new $className();
292276

293-
if (!empty($themeConfig->csrfExcept)) {
294-
$this->mergeCsrfExcept($themeConfig->csrfExcept);
295-
}
296-
297-
if (!empty($themeConfig->filters)) {
298-
$this->filters = array_merge($this->filters, $themeConfig->filters);
277+
foreach ($modules as $module) {
278+
$configClass = "Modules\\{$module}\\Config\\{$module}Config";
279+
if (class_exists($configClass)) {
280+
$configInstance = new $configClass();
281+
if (property_exists($configInstance, 'csrfExcept') && is_array($configInstance->csrfExcept)) {
282+
$allCsrfExcept = array_merge($allCsrfExcept, $configInstance->csrfExcept);
283+
}
284+
if (property_exists($configInstance, 'filters') && is_array($configInstance->filters)) {
285+
foreach ($configInstance->filters as $filterName => $filterRules) {
286+
if (!isset($allFilters[$filterName])) {
287+
$allFilters[$filterName] = $filterRules;
288+
} else {
289+
foreach ($filterRules as $filterType => $paths) {
290+
if (!isset($allFilters[$filterName][$filterType])) {
291+
$allFilters[$filterName][$filterType] = [];
292+
}
293+
$allFilters[$filterName][$filterType] = array_merge($allFilters[$filterName][$filterType], $paths);
299294
}
300295
}
301296
}
302297
}
303-
} catch (\Throwable $e) {
304-
// Ignore DB errors during install
298+
if (property_exists($configInstance, 'globals') && is_array($configInstance->globals)) {
299+
$allGlobals = array_merge($allGlobals, $configInstance->globals);
300+
}
301+
$allCsrfExcept = array_unique($allCsrfExcept);
305302
}
306303
}
304+
$this->filters = array_merge($this->filters, $allFilters);
305+
306+
// backendGuard'ın before rotalarını otomatik olarak after'a da yay.
307+
// Böylece CsrfTokenRefreshFilter alias içinde tanımlanmış tüm korumalı
308+
// rotalarda her module config'e ayrıca csrfTokenRefreshFilter eklemeye gerek kalmaz.
309+
if (!empty($this->filters['backendGuard']['before'])) {
310+
$this->filters['backendGuard']['after'] = array_unique([
311+
...($this->filters['backendGuard']['after'] ?? []),
312+
...$this->filters['backendGuard']['before'],
313+
]);
314+
}
315+
316+
$this->mergeCsrfExcept($allCsrfExcept);
317+
if (!empty($allGlobals)) {
318+
$this->globals = array_merge_recursive($this->globals, $allGlobals);
319+
}
307320
}
308321
}

app/Controllers/Home.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function index(string $seflink = '')
3131
$pages = $this->commonModel->lists(
3232
'pages',
3333
'pages.*, pages_langs.title, pages_langs.content, pages_langs.seo, pages_langs.seflink',
34-
['pages.id' => $homePageId, 'pages_langs.lang' => $locale],
34+
['pages.id' => $homePageId, 'pages_langs.lang' => $locale,'pages.isActive'=>1],
3535
'pages.id DESC', 1, 0, [], [],
3636
[['table' => 'pages_langs', 'cond' => 'pages_langs.pages_id = pages.id', 'type' => 'inner']],
3737
['isReset' => true]
@@ -41,7 +41,7 @@ public function index(string $seflink = '')
4141
$pages = $this->commonModel->lists(
4242
'pages',
4343
'pages.*, pages_langs.title, pages_langs.content, pages_langs.seo, pages_langs.seflink',
44-
['pages.id' => $homePageId, 'pages_langs.lang' => $defaultLang],
44+
['pages.id' => $homePageId, 'pages_langs.lang' => $defaultLang,'pages.isActive'=>1],
4545
'pages.id DESC', 1, 0, [], [],
4646
[['table' => 'pages_langs', 'cond' => 'pages_langs.pages_id = pages.id', 'type' => 'inner']],
4747
['isReset' => true]
@@ -52,7 +52,7 @@ public function index(string $seflink = '')
5252
$pages = $this->commonModel->lists(
5353
'pages',
5454
'pages.*, pages_langs.title, pages_langs.content, pages_langs.seo, pages_langs.seflink',
55-
['pages_langs.seflink' => $seflink, 'pages_langs.lang' => $locale],
55+
['pages_langs.seflink' => $seflink, 'pages_langs.lang' => $locale,'pages.isActive'=>1],
5656
'pages.id DESC', 1, 0, [], [],
5757
[['table' => 'pages_langs', 'cond' => 'pages_langs.pages_id = pages.id', 'type' => 'inner']],
5858
['isReset' => true]
@@ -62,7 +62,7 @@ public function index(string $seflink = '')
6262
$pages = $this->commonModel->lists(
6363
'pages',
6464
'pages.*, pages_langs.title, pages_langs.content, pages_langs.seo, pages_langs.seflink',
65-
['pages_langs.seflink' => $seflink, 'pages_langs.lang' => $defaultLang],
65+
['pages_langs.seflink' => $seflink, 'pages_langs.lang' => $defaultLang,'pages.isActive'=>1],
6666
'pages.id DESC', 1, 0, [], [],
6767
[['table' => 'pages_langs', 'cond' => 'pages_langs.pages_id = pages.id', 'type' => 'inner']],
6868
['isReset' => true]

app/Models/BlogModel.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ class BlogModel extends Model
1111

1212
public static function sitemapItems(): array
1313
{
14-
$blogs = model(self::class)->join('blog_langs','blog_langs.id = blog.id','left')->where(['isActive' => true, 'inXML' => true])->findAll();
14+
$where=['isActive' => true,'inXML' => true];
15+
if(setting()->get('App.siteLanguageMode')==='single'){
16+
$where['blog_langs.lang']=setting()->get('App.defaultLocale');
17+
}
18+
$blogs = model(self::class)->join('blog_langs','blog_langs.id = blog.id','left')->where($where)->findAll();
1519
$items = [];
1620

1721
foreach ($blogs as $blog) {

app/Models/PagesModel.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ class PagesModel extends Model
1111

1212
public static function sitemapItems(): array
1313
{
14-
$pages = model(self::class)->join('pages_langs','pages_langs.id = pages.id','left')->where(['isActive' => true])->orderBy('seflink ASC')->findAll();
14+
$where=['isActive' => true];
15+
if(setting()->get('App.siteLanguageMode')==='single'){
16+
$where['pages_langs.lang']=setting()->get('App.defaultLocale');
17+
}
18+
$pages = model(self::class)->join('pages_langs','pages_langs.id = pages.id','left')->where($where)->orderBy('seflink ASC')->findAll();
1519
$items = [];
1620

1721
foreach ($pages as $page) {

0 commit comments

Comments
 (0)