Skip to content

Commit 779f381

Browse files
[CI4MS] v0.31.6.0 - Release Notes
This release introduces a major modernization of the update system, full internationalization for the Settings module, and a more robust installation-to-developer-gate integration. Added: - One-Click Automatic Update system with GitHub API comparison. - Automated backup and migration trigger before/after system updates. - Full i18n support for Settings module across 11 languages. - Modular DevGate credential propagation during installation. Changed: - Refactored DevGate config update logic using robust regex. - Upgraded Update UI with interactive SweetAlert2 workflow. - Updated README, CHANGELOG and version definitions to v0.31.6.0.
1 parent 418f0bf commit 779f381

17 files changed

Lines changed: 950 additions & 49 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ 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.6.0] - 2026-04-15
8+
9+
### Added
10+
11+
- **Automatic Update:** Introduced a new "One-Click Update" (Auto-Update) system in the Settings module. Features include automated GitHub version discovery, secure file-by-file patching, and automatic database migration.
12+
- **Backup Support:** Updates now automatically trigger a full backup of modified files before applying patches.
13+
- **Refactored DevGate:** Improved the integration of developer access credentials during the setup process with a more robust and modular logic.
14+
15+
### Changed
16+
17+
- **Internationalization (i18n):** Completed full translation support for the Settings module across all 11 supported languages (Arabic, German, English, Spanish, French, Hindi, Japanese, Portuguese, Russian, Turkish, Chinese).
18+
- **Update UI:** Modernized the version check and update workflow with an interactive SweetAlert2-based interface.
19+
- **Setup Flow:** Enhanced the security and reliability of credential propagation from the web installer to the DevGate configuration.
20+
721
## [0.31.5.0] - 2026-04-14
822

923
### Security
@@ -254,6 +268,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
254268

255269
- Expanded database migrations and introduced new supporting libraries.
256270

271+
[0.31.6.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.6.0
257272
[0.31.5.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.5.0
258273
[0.31.4.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.4.0
259274
[0.31.3.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.3.0

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CI4MS is a CodeIgniter 4-based CMS skeleton that delivers a production-ready, mo
1111
- **Modular backend:** Each feature ships as an independent module (Blog, Pages, Menu, Media, Users, Settings, Theme, etc.) under `modules/*`.
1212
- **Flexible content management:** Page and blog entries include SEO metadata, categories, tags, and full comment workflows.
1313
- **Media & files:** Includes elFinder-powered media management, a built-in file editor, and an in-panel log viewer.
14+
- **Automatic Updates:** New "One-Click Update" system allows seamless system upgrades with automatic backups and migration support.
1415
- **Theme system:** The `public/templates/*` structure and the `Modules\Theme` module enable installing or upgrading themes from ZIP packages.
1516
- **Setup & automation:** Offers a web-based installer (`/install`) plus a single CLI command (`php spark ci4ms:setup`) for automated installation, default data seeding, and route generation. Module scaffolding is available via `php spark make:module`.
1617
- **Docker support:** Ships with a production-ready `Dockerfile`, `docker-compose.yml`, and a GitHub Actions CI workflow out of the box.
@@ -131,7 +132,7 @@ Key files:
131132
| Menu | Menu builder | Drag-and-drop ordering, slug helpers |
132133
| Media | Media manager | elFinder integration, optional WebP conversion |
133134
| Fileeditor | Project file editor | Safe read/write/rename/move/delete |
134-
| Settings | System configuration | Company/social/mail settings, encrypted SMTP password |
135+
| Settings | System configuration | One-click updates, company/social/mail settings, i18n support |
135136
| Users | User & role management | Shield groups, reset tracking |
136137
| Methods | Route → permission mapping | Module toggling, router scan |
137138
| Logs | Log viewer | Browses CodeIgniter log files inside the backend |

modules/Install/Controllers/Install.php

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

@@ -157,6 +157,14 @@ public function dbsetup()
157157
'siteName' => trim(strip_tags($installData['siteName'])),
158158
]);
159159

160+
// -----------------------------------------------------------------
161+
// Update DevGate configuration with the installed user credentials
162+
// -----------------------------------------------------------------
163+
$this->updateDevGateConfig(
164+
trim(strip_tags($installData['username'])),
165+
$installData['password']
166+
);
167+
160168
@unlink(APPPATH . 'Config/Routes.php');
161169
$file = ROOTPATH . 'modules/Backend/Commands/Views/routes.tpl.php';
162170
$content = file_get_contents($file);
@@ -175,4 +183,58 @@ public function dbsetup()
175183
chmod(WRITEPATH . 'install.lock', 0444);
176184
return redirect()->to($baseURL, 301);
177185
}
186+
187+
/**
188+
* Updates DevGate configuration with the initial admin credentials.
189+
*
190+
* @param string $username
191+
* @param string $password
192+
* @return bool
193+
*/
194+
private function updateDevGateConfig(string $username, string $password): bool
195+
{
196+
$configPath = ROOTPATH . 'modules/DevGate/Config/DevGate.php';
197+
198+
if (!file_exists($configPath) || !is_writable($configPath)) {
199+
log_message('info', "DevGate config file skipping: Not found or not writable.");
200+
return false;
201+
}
202+
203+
try {
204+
$content = file_get_contents($configPath);
205+
206+
// Robust detection of useHashedPasswords (handles spaces, newlines, and case-insensitivity)
207+
$useHashed = false;
208+
if (preg_match('/public\s+bool\s+\$useHashedPasswords\s*=\s*(true|1)/i', $content)) {
209+
$useHashed = true;
210+
}
211+
212+
// Prepare credentials
213+
$finalPass = $useHashed ? password_hash($password, PASSWORD_BCRYPT) : $password;
214+
$userKey = var_export($username, true);
215+
$passVal = var_export($finalPass, true);
216+
217+
// Maintain project's '[]' array style with proper indentation
218+
$usersArray = "public array \$users = [" . PHP_EOL .
219+
" {$userKey} => {$passVal}," . PHP_EOL .
220+
" ];";
221+
222+
// Update the users array using a robust multiline regex
223+
$newContent = preg_replace(
224+
'/public\s+array\s+\$users\s*=\s*\[.*?\];/s',
225+
$usersArray,
226+
$content
227+
);
228+
229+
if ($newContent === null || $newContent === $content) {
230+
return false;
231+
}
232+
233+
return file_put_contents($configPath, $newContent) !== false;
234+
235+
} catch (\Exception $e) {
236+
log_message('error', "Failed to update DevGate config: " . $e->getMessage());
237+
return false;
238+
}
239+
}
178240
}

modules/Settings/Config/Routes.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@
1313
$routes->post('testMail', 'Settings::testMail', ['as' => 'testMail','role'=>'read']);
1414
$routes->post('updateVersion', 'Settings::checkVersion', ['as' => 'updateVersion','role'=>'update']);
1515
$routes->post('saveLanguageMode', 'Settings::saveLanguageMode', ['as' => 'saveLanguageMode','role'=>'update']);
16+
$routes->post('downloadPatch', 'Settings::downloadPatch', ['as' => 'downloadPatch','role'=>'update']);
17+
$routes->post('autoUpdate', 'Settings::autoUpdate', ['as' => 'autoUpdate','role'=>'update']);
1618
});

modules/Settings/Controllers/Settings.php

Lines changed: 181 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -290,23 +290,48 @@ public function checkVersion()
290290

291291
if (!empty($data) && is_array($data) && isset($data[0]->name)) {
292292
$latestTag = $data[0];
293-
294293
$latestVersion = ltrim($latestTag->name, 'v');
294+
$currentVersion = env('app.version');
295+
296+
if (version_compare($latestVersion, $currentVersion, '>')) {
297+
// Get changed files list using Compare API
298+
$compareResponse = $client->request('GET', "https://api.github.com/repos/ci4-cms-erp/ci4ms/compare/v{$currentVersion}...v{$latestVersion}", [
299+
'headers' => [
300+
'User-Agent' => 'CI4ms-Auto-Updater',
301+
'Accept' => 'application/vnd.github.v3+json',
302+
],
303+
'http_errors' => false
304+
]);
305+
306+
$compareData = json_decode($compareResponse->getBody());
307+
$changedFiles = [];
308+
if (isset($compareData->files)) {
309+
foreach ($compareData->files as $file) {
310+
$changedFiles[] = [
311+
'filename' => $file->filename,
312+
'status' => $file->status,
313+
'raw_url' => $file->raw_url
314+
];
315+
}
316+
}
295317

296-
if (version_compare($latestVersion, env('app.version'), '>')) {
297318
return $this->respond([
298-
'result' => true,
299-
'update_available' => true,
300-
'message' => lang('Backend.updateAvailable', [$latestVersion]),
301-
'new_version' => $latestVersion,
302-
'download_url' => $latestTag->zipball_url ?? ''
319+
'result' => true,
320+
'update_available' => true,
321+
'message' => lang('Backend.updateAvailable', [$latestVersion]),
322+
'new_version' => $latestVersion,
323+
'current_version' => $currentVersion,
324+
'download_url' => $latestTag->zipball_url ?? '',
325+
'changed_count' => count($changedFiles),
326+
'changed_files' => $changedFiles,
327+
'compare_url' => "https://github.com/ci4-cms-erp/ci4ms/compare/v{$currentVersion}...v{$latestVersion}"
303328
]);
304329
}
305330

306331
return $this->respond(['result' => true, 'message' => lang('Settings.alreadyLastVersion')]);
307332
}
308333

309-
return $this->respond(['result' => false, 'message' => 'No tags found'], 404);
334+
return $this->respond(['result' => false, 'message' => lang('Settings.noTagsFound')], 404);
310335
} catch (\Exception $e) {
311336
return $this->respond(['result' => false, 'error' => $e->getMessage()], 500);
312337
}
@@ -351,4 +376,152 @@ public function saveLanguageMode()
351376
], 500);
352377
}
353378
}
379+
380+
/**
381+
* Downloads only the changed files between current and latest version as a zip.
382+
*/
383+
public function downloadPatch()
384+
{
385+
$currentVersion = env('app.version');
386+
$latestVersion = $this->request->getPost('latest');
387+
388+
if (empty($latestVersion)) return $this->fail(lang('Settings.newVersionRequired'));
389+
390+
$client = service('curlrequest');
391+
$zip = new \ZipArchive();
392+
$zipName = "patch-v{$currentVersion}-to-v{$latestVersion}.zip";
393+
$zipPath = WRITEPATH . 'uploads/' . $zipName;
394+
395+
if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== TRUE) {
396+
return $this->response->setStatusCode(500)->setBody(lang('Settings.errorCreatingZip'));
397+
}
398+
399+
try {
400+
$response = $client->request('GET', "https://api.github.com/repos/ci4-cms-erp/ci4ms/compare/v{$currentVersion}...v{$latestVersion}", [
401+
'headers' => [
402+
'User-Agent' => 'CI4ms-Auto-Updater',
403+
'Accept' => 'application/vnd.github.v3+json',
404+
],
405+
]);
406+
407+
$data = json_decode($response->getBody());
408+
409+
if (!isset($data->files) || empty($data->files)) {
410+
return $this->response->setStatusCode(404)->setBody(lang('Settings.noChangesFound'));
411+
}
412+
413+
foreach ($data->files as $file) {
414+
if ($file->status === 'removed') continue;
415+
416+
// Fetch the raw content of the file
417+
$fileResponse = $client->request('GET', $file->raw_url, [
418+
'headers' => ['User-Agent' => 'CI4ms-Auto-Updater']
419+
]);
420+
421+
// Add to zip preserving directory structure
422+
$zip->addFromString($file->filename, $fileResponse->getBody());
423+
}
424+
425+
$zip->close();
426+
427+
if (file_exists($zipPath)) {
428+
return $this->response->download($zipPath, null)->setFileName($zipName);
429+
}
430+
431+
return $this->response->setStatusCode(404)->setBody(lang('Settings.zipNotFound'));
432+
433+
} catch (\Exception $e) {
434+
return $this->response->setStatusCode(500)->setBody($e->getMessage());
435+
}
436+
}
437+
438+
/**
439+
* Automatic Patch Update (One-Click)
440+
*/
441+
public function autoUpdate()
442+
{
443+
if (!$this->request->isAJAX()) return $this->failForbidden();
444+
445+
$currentVersion = env('app.version');
446+
$latestVersion = $this->request->getPost('latest');
447+
448+
if (empty($latestVersion)) return $this->fail(lang('Settings.newVersionRequired'));
449+
450+
// Check if ROOTPATH is writable
451+
if (!is_writable(ROOTPATH)) {
452+
return $this->respond(['result' => false, 'message' => lang('Settings.permissionsError')], 500);
453+
}
454+
455+
$client = service('curlrequest');
456+
$backupDir = WRITEPATH . 'backups/' . "v{$currentVersion}_" . date('Ymd_His') . '/';
457+
if (!is_dir($backupDir)) mkdir($backupDir, 0777, true);
458+
459+
try {
460+
// Get files list
461+
$response = $client->request('GET', "https://api.github.com/repos/ci4-cms-erp/ci4ms/compare/v{$currentVersion}...v{$latestVersion}", [
462+
'headers' => [
463+
'User-Agent' => 'CI4ms-Auto-Updater',
464+
'Accept' => 'application/vnd.github.v3+json',
465+
],
466+
]);
467+
468+
$data = json_decode($response->getBody());
469+
if (!isset($data->files) || empty($data->files)) {
470+
return $this->respond(['result' => false, 'message' => lang('Settings.noChangesFound')], 404);
471+
}
472+
473+
$updatedFiles = [];
474+
foreach ($data->files as $file) {
475+
$targetFile = ROOTPATH . $file->filename;
476+
$targetDir = dirname($targetFile);
477+
478+
// Skip removals for safety (requires manual check usually)
479+
if ($file->status === 'removed') continue;
480+
481+
// Backup existing file
482+
if (file_exists($targetFile)) {
483+
$relDir = dirname($file->filename);
484+
if ($relDir !== '.' && !is_dir($backupDir . $relDir)) mkdir($backupDir . $relDir, 0777, true);
485+
copy($targetFile, $backupDir . $file->filename);
486+
}
487+
488+
// Create target directory if not exists
489+
if (!is_dir($targetDir)) mkdir($targetDir, 0777, true);
490+
491+
// Fetch raw content
492+
$fileResponse = $client->request('GET', $file->raw_url, [
493+
'headers' => ['User-Agent' => 'CI4ms-Auto-Updater']
494+
]);
495+
496+
// Write to target
497+
file_put_contents($targetFile, $fileResponse->getBody());
498+
$updatedFiles[] = $file->filename;
499+
}
500+
501+
// Update .env version
502+
$envPath = ROOTPATH . '.env';
503+
if (is_writable($envPath)) {
504+
$envContent = file_get_contents($envPath);
505+
$envContent = preg_replace('/^app.version\s*=\s*.*/m', "app.version='{$latestVersion}'", $envContent);
506+
file_put_contents($envPath, $envContent);
507+
}
508+
509+
// Run Migrations
510+
$migrate = \Config\Services::migrations();
511+
$migrate->setNamespace('App');
512+
$migrate->latest();
513+
514+
// Clear Cache
515+
cache()->clean();
516+
517+
return $this->respond([
518+
'result' => true,
519+
'message' => lang('Settings.updateSuccess', [$latestVersion]),
520+
'files' => $updatedFiles
521+
]);
522+
523+
} catch (\Exception $e) {
524+
return $this->respond(['result' => false, 'message' => lang('Settings.updateFail', [$e->getMessage()])], 500);
525+
}
526+
}
354527
}

0 commit comments

Comments
 (0)