From d421aa01f76651493215fe531cfe298f01b854ff Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 14:12:47 +0000 Subject: [PATCH 1/2] Phase 0: module skeleton for ImpressCMS 2.0 in IPF mode - icms_version.php with modname imforum and an (empty) object_items array, which makes the core register class/ under mod_imforum and manage the tables - include/common.php with IMFORUM_DB_VERSION, which the core reads during install and update without a defined() check - include/onupdate.php with the install/update entry points, an empty imforum_db_upgrade_1() and an upload folder created with 0755 - placeholder user index and administration index with templates - English and Dutch language files, Crowdin configuration for the rest - CI on PHP 8.2 to 8.5: php -l, PHPStan level 6 against a core checkout, PHPUnit tests for the module definition, install routines and language parity Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MZy45V5RV9nx6Buq8vqKHr --- .github/workflows/ci.yml | 49 +++++++++++++++++++ .gitignore | 5 ++ CHANGELOG.md | 5 ++ README.md | 35 ++++++++++++- composer.json | 26 ++++++++++ crowdin.yml | 15 ++++++ phpstan.neon.dist | 16 ++++++ phpunit.xml.dist | 19 ++++++++ src/admin/index.php | 23 +++++++++ src/admin/menu.php | 19 ++++++++ src/assets/css/imforum.css | 1 + src/assets/css/index.html | 1 + src/assets/index.html | 1 + src/assets/js/index.html | 1 + src/blocks/index.html | 1 + src/class/index.html | 1 + src/icms_version.php | 65 +++++++++++++++++++++++++ src/images/imforum_icon.png | Bin 0 -> 212 bytes src/images/imforum_iconbig.png | Bin 0 -> 410 bytes src/images/imforum_iconsmall.png | Bin 0 -> 107 bytes src/images/index.html | 1 + src/include/common.php | 26 ++++++++++ src/include/index.html | 1 + src/include/onupdate.php | 60 +++++++++++++++++++++++ src/index.php | 23 +++++++++ src/language/english/admin.php | 8 +++ src/language/english/common.php | 8 +++ src/language/english/index.html | 1 + src/language/english/main.php | 7 +++ src/language/english/modinfo.php | 8 +++ src/language/index.html | 1 + src/language/nederlands/admin.php | 8 +++ src/language/nederlands/common.php | 8 +++ src/language/nederlands/index.html | 1 + src/language/nederlands/main.php | 7 +++ src/language/nederlands/modinfo.php | 8 +++ src/templates/blocks/index.html | 1 + src/templates/imforum_admin_index.html | 13 +++++ src/templates/imforum_index.html | 4 ++ src/templates/index.html | 1 + tests/Unit/InstallRoutinesTest.php | 41 ++++++++++++++++ tests/Unit/LanguageParityTest.php | 40 +++++++++++++++ tests/Unit/ModuleDefinitionTest.php | 59 ++++++++++++++++++++++ tests/bootstrap.php | 57 ++++++++++++++++++++++ tests/phpstan-bootstrap.php | 22 +++++++++ 45 files changed, 696 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 composer.json create mode 100644 crowdin.yml create mode 100644 phpstan.neon.dist create mode 100644 phpunit.xml.dist create mode 100644 src/admin/index.php create mode 100644 src/admin/menu.php create mode 100644 src/assets/css/imforum.css create mode 100644 src/assets/css/index.html create mode 100644 src/assets/index.html create mode 100644 src/assets/js/index.html create mode 100644 src/blocks/index.html create mode 100644 src/class/index.html create mode 100644 src/icms_version.php create mode 100644 src/images/imforum_icon.png create mode 100644 src/images/imforum_iconbig.png create mode 100644 src/images/imforum_iconsmall.png create mode 100644 src/images/index.html create mode 100644 src/include/common.php create mode 100644 src/include/index.html create mode 100644 src/include/onupdate.php create mode 100644 src/index.php create mode 100644 src/language/english/admin.php create mode 100644 src/language/english/common.php create mode 100644 src/language/english/index.html create mode 100644 src/language/english/main.php create mode 100644 src/language/english/modinfo.php create mode 100644 src/language/index.html create mode 100644 src/language/nederlands/admin.php create mode 100644 src/language/nederlands/common.php create mode 100644 src/language/nederlands/index.html create mode 100644 src/language/nederlands/main.php create mode 100644 src/language/nederlands/modinfo.php create mode 100644 src/templates/blocks/index.html create mode 100644 src/templates/imforum_admin_index.html create mode 100644 src/templates/imforum_index.html create mode 100644 src/templates/index.html create mode 100644 tests/Unit/InstallRoutinesTest.php create mode 100644 tests/Unit/LanguageParityTest.php create mode 100644 tests/Unit/ModuleDefinitionTest.php create mode 100644 tests/bootstrap.php create mode 100644 tests/phpstan-bootstrap.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bc1757c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + test: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3', '8.4', '8.5'] + env: + ICMS_CORE_PATH: ${{ github.workspace }}/core/htdocs + steps: + - name: Checkout module + uses: actions/checkout@v4 + + - name: Checkout ImpressCMS core (signatures for PHPStan) + uses: actions/checkout@v4 + with: + repository: fiammybe/impresscms + ref: main + path: core + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: composer:v2 + + - name: Install dev dependencies + run: composer install --no-interaction --no-progress --prefer-dist + + - name: Lint + run: composer lint + + - name: Static analysis + run: composer analyse -- --no-progress --memory-limit=1G + + - name: Unit tests + run: composer test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a183671 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/vendor/ +/composer.lock +/.phpunit.cache/ +/.phpunit.result.cache +/.phpstan/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0b7a635 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.0.0 Alpha (unreleased) + +- Phase 0: module skeleton for ImpressCMS 2.0 in IPF mode, install and update routines, English and Dutch language files, CI on PHP 8.2 to 8.5. diff --git a/README.md b/README.md index 73c4d19..69740b2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,35 @@ # imforum -ImpressCMS forum based on IPF + +Community forum module for ImpressCMS 2.0, built on the ImpressCMS Persistable Framework (IPF). imforum is the successor of iForum: same features, new code, and an importer that takes over the data of a running iForum installation. + +Status: phase 0 (foundation). The module installs and updates, but has no forum functions yet. + +## Requirements + +- ImpressCMS 2.0 +- PHP 8.2 to 8.5 +- MySQL 5.5.3 or newer (utf8mb4); any storage engine, imforum does not rely on transactions or foreign keys + +## Installation + +Copy `src/` to `modules/imforum` in your ImpressCMS installation and install the module from System → Modules. The directory must be called `imforum`: IPF derives the table and class names from it, so the module cannot be cloned under another name. + +## Development + +```sh +composer install +composer lint # php -l on every file +composer test # PHPUnit +ICMS_CORE_PATH=/path/to/impresscms/htdocs composer analyse # PHPStan, needs a core checkout +``` + +Conventions: + +- Classes are named `mod_imforum_` and `mod_imforum_Handler` and live in `src/class/.php` and `src/class/Handler.php`; the core autoloads them once `object_items` is set. +- Form labels come from `_CO_IMFORUM__` in `language/*/common.php`. +- English and Dutch ship with the module; other languages go through Crowdin. +- Never rename or remove an object var in one release. The core drops columns that are no longer declared before the upgrade functions run, so add the new var and copy the data first, and remove the old var one release later. + +## License + +MIT, see [LICENSE](LICENSE). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a087a84 --- /dev/null +++ b/composer.json @@ -0,0 +1,26 @@ +{ + "name": "iforummodule/imforum", + "description": "Community forum module for ImpressCMS 2.0, built on the ImpressCMS Persistable Framework", + "type": "project", + "license": "MIT", + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5", + "phpstan/phpstan": "^2.1" + }, + "autoload-dev": { + "psr-4": { + "Imforum\\Tests\\": "tests/" + } + }, + "scripts": { + "lint": "find src tests -name '*.php' -print0 | xargs -0 -n1 php -l", + "analyse": "phpstan analyse", + "test": "phpunit" + }, + "config": { + "sort-packages": true + } +} diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 0000000..4290c52 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,15 @@ +files: + - source: /src/language/english/*.php + translation: /src/language/%two_letters_code%/%original_file_name% + languages_mapping: + two_letters_code: + nl: nederlands + fr: francais + it: italiano + de: german + es: spanish + pt-BR: portuguesebr + ru: russian + fa: persian + ja: ja_utf8 + zh-CN: schinese diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..a95e693 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,16 @@ +parameters: + level: 6 + paths: + - src + excludePaths: + - src/language/* + bootstrapFiles: + - tests/phpstan-bootstrap.php + # The ImpressCMS core is checked out next to the module in CI (see .github/workflows/ci.yml) + # and only scanned for class and function signatures, never analysed. + scanDirectories: + - %env.ICMS_CORE_PATH%/libraries/icms + scanFiles: + - %env.ICMS_CORE_PATH%/include/functions.php + - %env.ICMS_CORE_PATH%/include/cp_functions.php + - %env.ICMS_CORE_PATH%/libraries/icms.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..1646853 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,19 @@ + + + + + tests/Unit + + + + + src + + + diff --git a/src/admin/index.php b/src/admin/index.php new file mode 100644 index 0000000..0ab292e --- /dev/null +++ b/src/admin/index.php @@ -0,0 +1,23 @@ +displayAdminMenu(0, _AM_IMFORUM_INDEX); + +$icmsAdminTpl->assign('imforum_version', icms::$module->getVar('version')); +$icmsAdminTpl->assign('imforum_dbversion', icms::$module->getDBVersion()); +$icmsAdminTpl->display('db:imforum_admin_index.html'); + +icms_cp_footer(); diff --git a/src/admin/menu.php b/src/admin/menu.php new file mode 100644 index 0000000..77ba047 --- /dev/null +++ b/src/admin/menu.php @@ -0,0 +1,19 @@ + _MI_IMFORUM_MENU_INDEX, + 'link' => 'admin/index.php', + 'icon' => 'images/imforum_iconbig.png', + 'small' => 'images/imforum_iconsmall.png', +]; diff --git a/src/assets/css/imforum.css b/src/assets/css/imforum.css new file mode 100644 index 0000000..c399a3c --- /dev/null +++ b/src/assets/css/imforum.css @@ -0,0 +1 @@ +/* Forum-specific additions on top of the ImpressCMS 2.0 default theme. */ diff --git a/src/assets/css/index.html b/src/assets/css/index.html new file mode 100644 index 0000000..74b6f45 --- /dev/null +++ b/src/assets/css/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/index.html b/src/assets/index.html new file mode 100644 index 0000000..74b6f45 --- /dev/null +++ b/src/assets/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/js/index.html b/src/assets/js/index.html new file mode 100644 index 0000000..74b6f45 --- /dev/null +++ b/src/assets/js/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/blocks/index.html b/src/blocks/index.html new file mode 100644 index 0000000..74b6f45 --- /dev/null +++ b/src/blocks/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/class/index.html b/src/class/index.html new file mode 100644 index 0000000..74b6f45 --- /dev/null +++ b/src/class/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icms_version.php b/src/icms_version.php new file mode 100644 index 0000000..12183a5 --- /dev/null +++ b/src/icms_version.php @@ -0,0 +1,65 @@ + _MI_IMFORUM_NAME, + 'version' => '1.0.0', + 'status' => 'Alpha', + 'status_version' => '1.0.0 Alpha', + 'date' => '25 October 2026', + 'description' => _MI_IMFORUM_DESC, + 'author' => 'fiammybe', + 'author_realname' => 'David Janssens', + 'credits' => 'Based on the ideas of iForum, CBB and newbb', + 'license' => 'MIT, see LICENSE', + 'official' => false, + 'dirname' => basename(__DIR__), + 'modname' => IMFORUM_DIRNAME, + + 'iconsmall' => 'images/imforum_iconsmall.png', + 'iconbig' => 'images/imforum_iconbig.png', + 'image' => 'images/imforum_icon.png', + + 'support_site_url' => 'https://github.com/iForumModule/imforum', + 'support_site_name' => 'imforum on GitHub', + 'submit_bug' => 'https://github.com/iForumModule/imforum/issues', + 'submit_feature' => 'https://github.com/iForumModule/imforum/issues', + + // IPF objects, filled in phase 1. An empty array already enables IPF mode. + 'object_items' => [], + + 'onInstall' => 'include/onupdate.php', + 'onUpdate' => 'include/onupdate.php', + + 'hasAdmin' => 1, + 'adminindex' => 'admin/index.php', + 'adminmenu' => 'admin/menu.php', + + 'hasMain' => 1, + 'hasSearch' => 0, + 'hasComments' => 0, + 'hasNotification' => 0, + + 'templates' => [ + ['file' => 'imforum_index.html', 'description' => 'Forum index'], + ['file' => 'imforum_admin_index.html', 'description' => 'Administration index'], + ], + + 'blocks' => [], + 'config' => [], +]; diff --git a/src/images/imforum_icon.png b/src/images/imforum_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3eb4da323e644ef355dcbd5b45b7670fa9515858 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=vproLLn>~)y}gmQ!GMRwf&Xxt z&WW~#iyapzICMSU{Dkpe*N&6!CFeHf>0Ul|`=r z!9yH8qV5Sx3>cV?3QDN+955(kapPn*@Q`UVWH8`hU~*v4U^^fH6l6Fcz`()4sKCI| zz#zcD$=VxFLxPnSHK6ZZaL%_n0rH{YqaPxx?3KljJ b`)$84N<6(N8WghPHpl=^S3j3^P6fEd64Hs zL&#Z;wh5~x^G-Z4YKSm+#cC8|&~l0K?q_CKOQB`_4AO<3n|W&4?*er*c)I$ztaD0e F0sshistory.go(-1); \ No newline at end of file diff --git a/src/include/common.php b/src/include/common.php new file mode 100644 index 0000000..30dfa73 --- /dev/null +++ b/src/include/common.php @@ -0,0 +1,26 @@ +() step */ + define('IMFORUM_DB_VERSION', 1); +} diff --git a/src/include/index.html b/src/include/index.html new file mode 100644 index 0000000..74b6f45 --- /dev/null +++ b/src/include/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/include/onupdate.php b/src/include/onupdate.php new file mode 100644 index 0000000..deba49c --- /dev/null +++ b/src/include/onupdate.php @@ -0,0 +1,60 @@ +() for every new database version; + * 3. icms_module_install_imforum() or icms_module_update_imforum() runs. + * + * Because the tables are synchronised before the upgrade functions run, a column + * that disappears from an object is dropped before any upgrade function sees it. + * Rename fields over two releases: add the new var and copy the data first, drop + * the old var in the next release. + * + * @copyright The iForum project + * @license MIT, see LICENSE + * @package imforum + * @since 1.0 + */ + +defined('ICMS_ROOT_PATH') || die('ImpressCMS root path not defined'); + +include_once __DIR__ . '/common.php'; + +/** + * Database version 1: indexes that the IPF updater does not create + * + * Filled in phase 1, once the objects exist. + */ +function imforum_db_upgrade_1(): bool { + return true; +} + +/** + * Runs once after a fresh install + */ +function icms_module_install_imforum(icms_module_Object $module): bool { + return imforum_create_upload_folder(); +} + +/** + * Runs after every module update + * + * @param string|null $oldversion module version before the update + * @param int|null $olddbversion database version before the update + */ +function icms_module_update_imforum(icms_module_Object $module, ?string $oldversion = null, ?int $olddbversion = null): bool { + return imforum_create_upload_folder(); +} + +/** + * Make sure uploads/imforum exists, without granting world-write access + */ +function imforum_create_upload_folder(): bool { + if (is_dir(IMFORUM_UPLOAD_PATH)) { + return true; + } + return mkdir(IMFORUM_UPLOAD_PATH, 0755, true); +} diff --git a/src/index.php b/src/index.php new file mode 100644 index 0000000..7861b2f --- /dev/null +++ b/src/index.php @@ -0,0 +1,23 @@ +assign('imforum_module_url', IMFORUM_URL); + +include_once ICMS_ROOT_PATH . '/footer.php'; diff --git a/src/language/english/admin.php b/src/language/english/admin.php new file mode 100644 index 0000000..4c927ff --- /dev/null +++ b/src/language/english/admin.php @@ -0,0 +1,8 @@ +_ and _CO_IMFORUM___DSC + * automatically as caption and description (icms_ipf_Object::initVar). + * Filled in phase 1. + */ diff --git a/src/language/english/index.html b/src/language/english/index.html new file mode 100644 index 0000000..74b6f45 --- /dev/null +++ b/src/language/english/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/language/english/main.php b/src/language/english/main.php new file mode 100644 index 0000000..8e83115 --- /dev/null +++ b/src/language/english/main.php @@ -0,0 +1,7 @@ +history.go(-1); \ No newline at end of file diff --git a/src/language/nederlands/admin.php b/src/language/nederlands/admin.php new file mode 100644 index 0000000..308c02b --- /dev/null +++ b/src/language/nederlands/admin.php @@ -0,0 +1,8 @@ +_ en _CO_IMFORUM___DSC + * automatisch als label en omschrijving (icms_ipf_Object::initVar). + * Wordt gevuld in fase 1. + */ diff --git a/src/language/nederlands/index.html b/src/language/nederlands/index.html new file mode 100644 index 0000000..74b6f45 --- /dev/null +++ b/src/language/nederlands/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/language/nederlands/main.php b/src/language/nederlands/main.php new file mode 100644 index 0000000..ab74c73 --- /dev/null +++ b/src/language/nederlands/main.php @@ -0,0 +1,7 @@ +history.go(-1); \ No newline at end of file diff --git a/src/templates/imforum_admin_index.html b/src/templates/imforum_admin_index.html new file mode 100644 index 0000000..d23ad70 --- /dev/null +++ b/src/templates/imforum_admin_index.html @@ -0,0 +1,13 @@ +
+

<{$smarty.const._AM_IMFORUM_INDEX}>

+ + + + + + + + + +
<{$smarty.const._AM_IMFORUM_VERSION}><{$imforum_version}>
<{$smarty.const._AM_IMFORUM_DBVERSION}><{$imforum_dbversion}>
+
diff --git a/src/templates/imforum_index.html b/src/templates/imforum_index.html new file mode 100644 index 0000000..f8970b9 --- /dev/null +++ b/src/templates/imforum_index.html @@ -0,0 +1,4 @@ +
+

<{$smarty.const._MD_IMFORUM_TITLE}>

+

<{$smarty.const._MD_IMFORUM_UNDER_CONSTRUCTION}>

+
diff --git a/src/templates/index.html b/src/templates/index.html new file mode 100644 index 0000000..74b6f45 --- /dev/null +++ b/src/templates/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/Unit/InstallRoutinesTest.php b/tests/Unit/InstallRoutinesTest.php new file mode 100644 index 0000000..5da6c66 --- /dev/null +++ b/tests/Unit/InstallRoutinesTest.php @@ -0,0 +1,41 @@ +_. + self::assertTrue(function_exists('icms_module_install_imforum')); + self::assertTrue(function_exists('icms_module_update_imforum')); + } + + public function testUpgradeFunctionExistsForEveryDatabaseVersion(): void + { + for ($version = 1; $version <= IMFORUM_DB_VERSION; $version++) { + self::assertTrue(function_exists('imforum_db_upgrade_' . $version), 'missing imforum_db_upgrade_' . $version); + } + } + + public function testUploadFolderIsCreatedWithoutWorldWrite(): void + { + self::assertTrue(imforum_create_upload_folder()); + self::assertDirectoryExists(IMFORUM_UPLOAD_PATH); + self::assertSame(0, fileperms(IMFORUM_UPLOAD_PATH) & 0002, 'upload folder must not be world-writable'); + // a second call is a no-op + self::assertTrue(imforum_create_upload_folder()); + } +} diff --git a/tests/Unit/LanguageParityTest.php b/tests/Unit/LanguageParityTest.php new file mode 100644 index 0000000..eed29d5 --- /dev/null +++ b/tests/Unit/LanguageParityTest.php @@ -0,0 +1,40 @@ + */ + public static function languageFiles(): iterable + { + foreach (glob(dirname(__DIR__, 2) . '/src/language/english/*.php') ?: [] as $file) { + yield basename($file) => [basename($file)]; + } + } + + #[DataProvider('languageFiles')] + public function testDutchDefinesEveryEnglishConstant(string $file): void + { + $english = self::constantsIn(dirname(__DIR__, 2) . '/src/language/english/' . $file); + $dutch = self::constantsIn(dirname(__DIR__, 2) . '/src/language/nederlands/' . $file); + + self::assertSame([], array_values(array_diff($english, $dutch)), "missing in nederlands/$file"); + self::assertSame([], array_values(array_diff($dutch, $english)), "unknown in nederlands/$file"); + } + + /** @return list */ + private static function constantsIn(string $path): array + { + self::assertFileExists($path); + preg_match_all("/define\\(\\s*['\"]([A-Z0-9_]+)['\"]/", (string) file_get_contents($path), $matches); + return $matches[1]; + } +} diff --git a/tests/Unit/ModuleDefinitionTest.php b/tests/Unit/ModuleDefinitionTest.php new file mode 100644 index 0000000..fef906f --- /dev/null +++ b/tests/Unit/ModuleDefinitionTest.php @@ -0,0 +1,59 @@ + */ + private static array $modversion; + + public static function setUpBeforeClass(): void + { + $modversion = []; + include IMFORUM_TEST_MODULE_PATH . '/icms_version.php'; + self::$modversion = $modversion; + } + + public function testDirnameAndModnameMatch(): void + { + // The IPF updater names tables after the dirname, the handlers after the modname. + self::assertSame('imforum', self::$modversion['dirname']); + self::assertSame('imforum', self::$modversion['modname']); + } + + public function testObjectItemsEnablesIpfMode(): void + { + // icms_module_Object::loadInfo sets ipf = 1 only when object_items is an array. + self::assertIsArray(self::$modversion['object_items']); + } + + public function testReferencedFilesExist(): void + { + foreach (['onInstall', 'onUpdate', 'adminindex', 'adminmenu', 'iconsmall', 'iconbig', 'image'] as $key) { + self::assertFileExists(IMFORUM_TEST_MODULE_PATH . '/' . self::$modversion[$key], $key); + } + } + + public function testEveryTemplateExists(): void + { + self::assertNotEmpty(self::$modversion['templates']); + foreach (self::$modversion['templates'] as $template) { + self::assertFileExists(IMFORUM_TEST_MODULE_PATH . '/templates/' . $template['file']); + } + } + + public function testDatabaseVersionIsDefined(): void + { + // moduleUpgrade() reads it with constant() and would throw if it were missing. + self::assertTrue(defined('IMFORUM_DB_VERSION')); + self::assertIsInt(IMFORUM_DB_VERSION); + self::assertGreaterThanOrEqual(1, IMFORUM_DB_VERSION); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..f96474f --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,57 @@ + Date: Sun, 25 Oct 2026 20:25:03 +0000 Subject: [PATCH 2/2] Fix admin index fatal error and test findings from the first install - admin/menu.php defines $headermenu; displayAdminMenu() counts it and fails on PHP 8 when it is null - explain that the core skips the table sync and db upgrade steps while object_items is empty, so dbversion stays 0 until phase 1 - skip the world-write check on Windows, where every folder reports 0777 - README: ICMS_CORE_PATH for PHPStan, copy instead of symlink for a test site Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MZy45V5RV9nx6Buq8vqKHr --- README.md | 4 +++ src/admin/menu.php | 8 ++++++ src/icms_version.php | 4 ++- src/include/onupdate.php | 5 +++- tests/Unit/AdminMenuTest.php | 42 ++++++++++++++++++++++++++++++ tests/Unit/InstallRoutinesTest.php | 3 +++ 6 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/AdminMenuTest.php diff --git a/README.md b/README.md index 69740b2..3f9a944 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ composer test # PHPUnit ICMS_CORE_PATH=/path/to/impresscms/htdocs composer analyse # PHPStan, needs a core checkout ``` +PHPStan reads the core classes from `ICMS_CORE_PATH`. Without that variable it stops with an error about a missing scan directory. + +For a local test site, copy `src/` to `modules/imforum`. A symlink or Windows junction does not work: PHP resolves `__DIR__` to the real folder, and the module takes its name from that folder. + Conventions: - Classes are named `mod_imforum_` and `mod_imforum_Handler` and live in `src/class/.php` and `src/class/Handler.php`; the core autoloads them once `object_items` is set. diff --git a/src/admin/menu.php b/src/admin/menu.php index 77ba047..c3f2f7c 100644 --- a/src/admin/menu.php +++ b/src/admin/menu.php @@ -17,3 +17,11 @@ 'icon' => 'images/imforum_iconbig.png', 'small' => 'images/imforum_iconsmall.png', ]; + +// icms_module_Object::displayAdminMenu() counts the header menu without an +// isset check, so it must always be an array. +$headermenu = []; +$headermenu[] = [ + 'title' => _MI_IMFORUM_NAME, + 'link' => ICMS_URL . '/modules/' . basename(dirname(__DIR__)) . '/', +]; diff --git a/src/icms_version.php b/src/icms_version.php index 12183a5..a367797 100644 --- a/src/icms_version.php +++ b/src/icms_version.php @@ -40,7 +40,9 @@ 'submit_bug' => 'https://github.com/iForumModule/imforum/issues', 'submit_feature' => 'https://github.com/iForumModule/imforum/issues', - // IPF objects, filled in phase 1. An empty array already enables IPF mode. + // IPF objects, filled in phase 1. An empty array already enables IPF mode + // (autoloading), but the core only runs the table sync and the + // imforum_db_upgrade_() steps once the array is not empty. 'object_items' => [], 'onInstall' => 'include/onupdate.php', diff --git a/src/include/onupdate.php b/src/include/onupdate.php index deba49c..e6f0b8e 100644 --- a/src/include/onupdate.php +++ b/src/include/onupdate.php @@ -5,7 +5,10 @@ * Order of events in the core (modules/system/admin/modulesadmin/modulesadmin.php): * 1. this file is included; * 2. moduleUpgrade($module, TRUE) creates or alters the tables of all object_items, - * then calls imforum_db_upgrade_() for every new database version; + * then calls imforum_db_upgrade_() for every new database version. The core + * skips this step while object_items is empty (modulesadmin.php checks + * !empty()), so until phase 1 adds objects the stored dbversion stays 0 and + * imforum_db_upgrade_1() first runs on the update that adds them; * 3. icms_module_install_imforum() or icms_module_update_imforum() runs. * * Because the tables are synchronised before the upgrade functions run, a column diff --git a/tests/Unit/AdminMenuTest.php b/tests/Unit/AdminMenuTest.php new file mode 100644 index 0000000..3ef293b --- /dev/null +++ b/tests/Unit/AdminMenuTest.php @@ -0,0 +1,42 @@ +> */ + private static array $adminmenu; + + /** @var array> */ + private static array $headermenu; + + public static function setUpBeforeClass(): void + { + $adminmenu = $headermenu = null; + include IMFORUM_TEST_MODULE_PATH . '/admin/menu.php'; + self::$adminmenu = $adminmenu; + self::$headermenu = $headermenu; + } + + public function testAdminMenuHasEntries(): void + { + self::assertNotEmpty(self::$adminmenu); + } + + public function testHeaderMenuIsANonEmptyArray(): void + { + // displayAdminMenu() calls count() on it; null is a fatal error on PHP 8. + self::assertNotEmpty(self::$headermenu); + foreach (self::$headermenu as $item) { + self::assertArrayHasKey('title', $item); + self::assertArrayHasKey('link', $item); + } + } +} diff --git a/tests/Unit/InstallRoutinesTest.php b/tests/Unit/InstallRoutinesTest.php index 5da6c66..f9503c8 100644 --- a/tests/Unit/InstallRoutinesTest.php +++ b/tests/Unit/InstallRoutinesTest.php @@ -32,6 +32,9 @@ public function testUpgradeFunctionExistsForEveryDatabaseVersion(): void public function testUploadFolderIsCreatedWithoutWorldWrite(): void { + if (PHP_OS_FAMILY === 'Windows') { + self::markTestSkipped('Windows reports every directory as 0777'); + } self::assertTrue(imforum_create_upload_folder()); self::assertDirectoryExists(IMFORUM_UPLOAD_PATH); self::assertSame(0, fileperms(IMFORUM_UPLOAD_PATH) & 0002, 'upload folder must not be world-writable');