Skip to content

Commit 9874f43

Browse files
feat:Notification Center,cache management,settings-decode hardening
Added - Notifications module (Modules\Notifications): in-app admin notifications on a Model B design - one global row per notification, per-user read state in notification_reads, and Notifier::applyRelevance() as the single relevance/IDOR chokepoint shared by every read path. * Realtime delivery over Redis-backed SSE (no external hub, no JWT, no broker), disabled by default via NotificationsConfig::$realtimeEnabled; the client never trusts the payload and always reconciles against the DB. * Role-aware cap on concurrent SSE connections, enforced with a single atomic Lua EVAL over a Redis sorted set; fail-closed when Redis is unreachable. * Rich targeting (several users and groups in one dispatch, exceptUser() exclusions) and per-user opt-out preferences applied at read time; critical notifications cannot be muted. * Admin composer screen with server-side audience validation, recipient-count preview, and a created_by accountability column. - Settings -> Cache Management panel backed by a server-side allowlist (Modules\Settings\Libraries\CacheRegistry): the client sends only logical ids and glob patterns are resolved on the server, the framework-wide cache:clear / clean() is never invoked, and the Shield RBAC config key is protected.
1 parent 653cad7 commit 9874f43

71 files changed

Lines changed: 6910 additions & 56 deletions

File tree

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: 39 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ Key files:
154154
| Backup | Database backup manager | Create, download, and restore with SQL sanitization |
155155
| DashboardWidgets | Dashboard statistics | Modular widget system for admin overview |
156156
| LanguageManager | Language file manager | Edit and manage translation files from the backend |
157+
| Notifications | In-app admin notifications | Bell dropdown, single-global-row targeting (user / group / broadcast), optional Redis-backed SSE realtime, per-user opt-out screen |
157158

158159
See `docs/architecture.md` for deeper architectural notes.
159160

app/Config/Events.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,22 @@
5555
}
5656
}
5757
});
58+
59+
Events::on('ci4ms.audit', static function (array $e) {
60+
if (($e['severity'] ?? '') !== 'warning') {
61+
return;
62+
}
63+
64+
$group = config('Modules\Notifications\Config\NotificationsConfig')->auditTargetGroup ?? 'superadmin';
65+
66+
try {
67+
service('notifier')?->notify('audit.' . ($e['action'] ?? 'event'))
68+
->severity('warning')
69+
->title($e['message'] ?? '')
70+
->url($e['url'] ?? null)
71+
->toGroup($group)
72+
->dispatch();
73+
} catch (\Throwable $ex) {
74+
log_message('error', 'ci4ms.audit notifier dispatch failed: ' . $ex->getMessage());
75+
}
76+
});

app/Config/Filters.php

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,11 @@ public function __construct()
134134
if (empty(cache('settings')) && $this->commonModel->db->tableExists('settings')) {
135135
$this->settings = $this->commonModel->lists('settings');
136136
$set = [];
137-
$formatRules = new \CodeIgniter\Validation\FormatRules();
138137
foreach ($this->settings as $setting) {
139-
if ($formatRules->valid_json($setting->value) === true)
140-
$set[$setting->key] = (object) json_decode($setting->value, JSON_UNESCAPED_UNICODE);
141-
else
142-
$set[$setting->key] = $setting->value;
138+
$decoded = json_decode($setting->value);
139+
$set[$setting->key] = (json_last_error() === JSON_ERROR_NONE && (is_object($decoded) || is_array($decoded)))
140+
? $decoded
141+
: $setting->value;
143142
}
144143
cache()->save('settings', $set, 86400);
145144
$this->settings = (object) $set;

app/Config/Format.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,17 @@ class Format extends BaseConfig
6161
'application/xml' => 0,
6262
'text/xml' => 0,
6363
];
64+
65+
/**
66+
* --------------------------------------------------------------------------
67+
* JSON encode depth
68+
* --------------------------------------------------------------------------
69+
*
70+
* Maximum nesting depth JSONFormatter passes to json_encode(). CI 4.7.4's
71+
* JSONFormatter reads this without a fallback, so the property must exist
72+
* or every respond()/setJSON() JSON output throws.
73+
*
74+
* @var int
75+
*/
76+
public int $jsonEncodeDepth = 512;
6477
}

app/Controllers/Home.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public function index(string $seflink = '')
9696
'telephone' => $this->defData['settings']->contact->phone,
9797
'contactType' => 'customer support'
9898
],
99-
'sameAs' => array_map(fn($sN) => $sN['link'], (array)$this->defData['settings']->socialNetwork)
99+
'sameAs' => array_map(fn($sN) => $sN->link, (array)$this->defData['settings']->socialNetwork)
100100
]
101101
);
102102
if (!empty($this->defData['pageInfo']->seo->coverImage))
@@ -113,7 +113,7 @@ public function index(string $seflink = '')
113113

114114
public function maintenanceMode()
115115
{
116-
if ((bool)$this->defData['settings']->maintenanceMode->scalar === false) return redirect()->route('home');
116+
if ((bool)($this->defData['settings']->maintenanceMode ?? false) === false) return redirect()->route('home');
117117
return view('maintenance', $this->defData);
118118
}
119119

@@ -157,7 +157,7 @@ public function blog(int $page = 1)
157157
'telephone' => $this->defData['settings']->contact->phone,
158158
'contactType' => 'customer support'
159159
],
160-
'sameAs' => array_map(fn($sN) => $sN['link'], (array)$this->defData['settings']->socialNetwork)
160+
'sameAs' => array_map(fn($sN) => $sN->link, (array)$this->defData['settings']->socialNetwork)
161161
]
162162
);
163163
$this->seo()->addSchema(SchemaPreset::breadcrumbs($this->commonLibrary->get_breadcrumbs('/blog/1', 'page')));
@@ -227,7 +227,7 @@ public function blogDetail(string $seflink)
227227
'telephone' => $this->defData['settings']->contact->phone,
228228
'contactType' => 'customer support'
229229
],
230-
'sameAs' => array_map(fn($sN) => $sN['link'], (array)$this->defData['settings']->socialNetwork)
230+
'sameAs' => array_map(fn($sN) => $sN->link, (array)$this->defData['settings']->socialNetwork)
231231
]
232232
));
233233
$this->seo()->addSchema(SchemaPreset::breadcrumbs($this->commonLibrary->get_breadcrumbs((int)$this->defData['infos']->id, 'blog')));
@@ -276,7 +276,7 @@ public function tagList(string $seflink, int $page = 1)
276276
'telephone' => $this->defData['settings']->contact->phone,
277277
'contactType' => 'customer support'
278278
],
279-
'sameAs' => array_map(fn($sN) => $sN['link'], (array)$this->defData['settings']->socialNetwork)
279+
'sameAs' => array_map(fn($sN) => $sN->link, (array)$this->defData['settings']->socialNetwork)
280280
]
281281
);
282282
$this->seo()->addSchema(SchemaPreset::breadcrumbs($this->commonLibrary->get_breadcrumbs($this->defData['tagInfo']->id, 'tag')));
@@ -334,7 +334,7 @@ public function category(string $seflink, int $page = 1)
334334
'telephone' => $this->defData['settings']->contact->phone,
335335
'contactType' => 'customer support'
336336
],
337-
'sameAs' => array_map(fn($sN) => $sN['link'], (array)$this->defData['settings']->socialNetwork)
337+
'sameAs' => array_map(fn($sN) => $sN->link, (array)$this->defData['settings']->socialNetwork)
338338
]
339339
);
340340
$this->defData['breadcrumbs'] = $this->commonLibrary->get_breadcrumbs((int)$this->defData['category']->id, 'category');

app/Filters/Ci4ms.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function before(RequestInterface $request, $arguments = null)
4040
return redirect()->to($protocol . $_SERVER['SERVER_NAME'] . '/install');
4141

4242
}
43-
if ((bool) cache()->get('settings')['maintenanceMode']->scalar === true)
43+
if ((bool) cache()->get('settings')['maintenanceMode'] === true)
4444
return redirect()->route('maintenance-mode');
4545
}
4646

app/Views/templates/default/base.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
<link rel="icon" type="image/x-icon"
99
href="<?php echo base_url('templates/default/assets/vendor/modern-business/favicon.ico') ?>" />
1010

11-
<?php if (!empty($settings->templateInfos->fonts['googleFont'])):
12-
$gf = urlencode($settings->templateInfos->fonts['googleFont']);
13-
$gw = $settings->templateInfos->fonts['weights'] ?? '400,600,700'; ?>
11+
<?php if (!empty($settings->templateInfos->fonts->googleFont)):
12+
$gf = urlencode($settings->templateInfos->fonts->googleFont);
13+
$gw = $settings->templateInfos->fonts->weights ?? '400,600,700'; ?>
1414
<link href="https://fonts.googleapis.com/css2?family=<?php echo $gf ?>:wght@<?php echo esc($gw) ?>&display=swap"
1515
rel="stylesheet">
1616
<?php endif;
17-
if (!empty($settings->templateInfos->theme_assets['styles'])):
18-
foreach ($settings->templateInfos->theme_assets['styles'] as $styleUrl):
17+
if (!empty($settings->templateInfos->theme_assets->styles)):
18+
foreach ($settings->templateInfos->theme_assets->styles as $styleUrl):
1919
$styleUrl = (str_starts_with($styleUrl, 'http') || str_starts_with($styleUrl, '//')) ? $styleUrl : base_url(ltrim($styleUrl, '/')); ?>
2020
<link href="<?php echo esc($styleUrl) ?>" rel="stylesheet" />
2121
<?php endforeach;
@@ -167,8 +167,8 @@ class="d-none d-xl-inline"><?php echo lang('Frontend.search') ?></span>
167167
</div>
168168
</div>
169169

170-
<?php if (!empty($settings->templateInfos->theme_assets['scripts'])):
171-
foreach ($settings->templateInfos->theme_assets['scripts'] as $scriptUrl):
170+
<?php if (!empty($settings->templateInfos->theme_assets->scripts)):
171+
foreach ($settings->templateInfos->theme_assets->scripts as $scriptUrl):
172172
$scriptUrl = (str_starts_with($scriptUrl, 'http') || str_starts_with($scriptUrl, '//')) ? $scriptUrl : base_url(ltrim($scriptUrl, '/')); ?>
173173
<script src="<?php echo esc($scriptUrl) ?>"></script>
174174
<?php endforeach;
@@ -178,7 +178,7 @@ class="d-none d-xl-inline"><?php echo lang('Frontend.search') ?></span>
178178
<script src="<?php echo base_url('templates/default/assets/vendor/bootstrap/bootstrap.bundle.min.js') ?>"></script>
179179
<?php endif;
180180
echo script_tag('be-assets/plugins/jquery-ui/jquery-ui.min.js');
181-
if (!empty($settings->templateInfos->display['backToTop'])): ?>
181+
if (!empty($settings->templateInfos->display->backToTop)): ?>
182182
<button id="back-to-top"
183183
style="position:fixed;bottom:90px;right:28px;z-index:1040;width:40px;height:40px;border-radius:50%;background:#804f7b;color:#fff;border:none;cursor:pointer;display:none;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,.3);font-size:18px;"
184184
title="Üste Dön" onclick="window.scrollTo({top:0,behavior:'smooth'})">↑</button>

app/Views/templates/default/blog/list.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
<section class="py-5">
3434
<div class="container">
3535
<div class="row">
36-
<div class="<?php echo (!empty($settings->templateInfos->widgets['sidebar'])) ? 'col-md-9' : 'col-md-12' ?>">
36+
<div class="<?php echo (!empty($settings->templateInfos->widgets->sidebar)) ? 'col-md-9' : 'col-md-12' ?>">
3737
<div class="px-5">
3838
<div class="row gx-5">
3939
<?php foreach ($blogs as $blog):
@@ -75,7 +75,7 @@
7575
</div>
7676
</div>
7777
</div>
78-
<?php if (!empty($settings->templateInfos->widgets['sidebar']))
78+
<?php if (!empty($settings->templateInfos->widgets->sidebar))
7979
echo view('templates/default/widgets/sidebar'); ?>
8080
</div>
8181
</div>

0 commit comments

Comments
 (0)