Files
Adam Daley 0eb833e026 Reorganise FOSSBilling core library (#4210)
* refactor(library): reorganize FOSSBilling core library

Group Update* prefix into FOSSBilling\Update\{Updater,Patcher,Finalization,ReadinessCheck}
Group Pagination into FOSSBilling\Pagination\{Service,Options,ViewPaginator}
Group System (Config,Environment,Version,Requirements) into FOSSBilling\System
Consolidate Security (Crypt,PasswordManager,Fingerprint,Session,CheckResult/Enum)
Group Exception into FOSSBilling\Exception\{BaseException,InformationException}
Fix I18n case (i18n.php -> I18n/I18n.php) and move Translate -> I18n/Translator
Group Logging (Logger, Monolog -> Factory)
Group Remote (CentralAlerts,ExtensionManager), move ErrorPage -> Http, InjectionAwareInterface -> Interfaces
Update all call sites (di.php, load.php, modules, tests) with no BC shims
Fix import aliasing and phpstan docblocks, remove unused imports
Tests: 2856 passed, phpstan OK

* refactor(validation): migrate Validate into Validation namespace

Split monolithic FOSSBilling\Validate (232 lines, 7 mixed responsibilities)
into focused validators:
- Validation\DomainValidator (isSldValid, isTldValid with DI cache/http)
- Validation\PasswordValidator (isPasswordStrong, passwordsMatch)
- Validation\EmailValidator (isEmailValid)
- Validation\BirthdayValidator (isBirthdayValid)
- Validation\RequiredValidator (checkRequiredParamsForArray)
- Validation\Validator facade retaining same API for $di['validator'] BC-free

Move FOSSBilling\Validate -> FOSSBilling\Validation\Validator,
add FOSSBilling\Validation\DomainValidator via $di['domainValidator'],
keep $di['validator'] as facade. Remove old Validate.php.
Update all call sites and tests (FOSSBilling\Validate -> FOSSBilling\Validation\Validator),
make Validator/DomainValidator non-final for Mockery.
Tests: 2856 passed, phpstan OK

* refactor(tools): split Tools into Utils/Security/Sanitizer/Validation (WIP)

Extract Tools god class (534 lines) into focused helpers:
- Utils\Str (slug, toCamelCase, fromCamelCase)
- Utils\Arr (safeCount, sortByOneKey, getPairsForTableByIds via arr service)
- Utils\Normalizer (normalizeBoolean, normalizePort)
- Utils\Network (isValidHttpInterface, getDefaultInterface, listHttpInterfaces, getExternalIP via network service)
- Utils\Format (humanReadableBytes)
- Security\Credential (generatePassword, session restore tokens)
- Sanitizer\Sanitizer (sanitizeContent, sanitizePlainText, sanitizeMarkdownContent)
- Validation\PhoneValidator + EmailValidator::validateAndSanitizeEmail

Update di.php: replace tools service with arr/network services,
migrate 23 src files to new helpers (FOSSBilling\Utils etc.),
keep Tools.php for remaining tests (to be removed next).
Tests: partial, 11 errors remain for slug mocks (to be fixed).

* refactor(api): introduce Role enum, tighten Api layer (deslopped)

- Add FOSSBilling\Api\Role enum (guest/client/admin) replacing stringly-typed role handling
- Api\Identity now stores Role, exposes getRole():Role + getType():string (BC), typeFromObject():Role
- Api\AbstractApi: make abstract, typed properties (string $ip, ?Module $module, Client|Admin|Guest|null $identity), typed setters/getters, fix BaseException usage
- Api\Proxy: compose Identity VO instead of duplicating type logic, keep lazy dispatcher from container, remove redundant helpers
- Api\Dispatcher: use Role enum and Identity VO, normalize identity, keep direct $di['update_finalization'] lookup, deslop finalization injection and apiClassSuffix helper
- Update tests/Pest IdentityTest to expect Role enum
- Minor: fix Cron/Staff Api call sites for typed identity

Deslop: removed hasModule/hasIdentity/hasService helpers, duplicate forwarding comment, unnecessary setFinalization/constructor injection and apiClassSuffix helper.

* refactor(cache): introduce Driver enum, use typed driver handling

- Add FOSSBilling\Cache\Driver enum (filesystem/redis/memcached)
- CacheFactory: parse config via Driver::tryFrom, match on enum, use ->value for messages, remove unused Tools import
- System\Api\Admin: validate driver via Driver::tryFrom instead of in_array(SUPPORTED_DRIVERS)

* refactor(doctrine+api): address review findings

- Doctrine: remove unused Tools import in DriverManagerFactory
- Doctrine: add Driver/Charset enums (pdo_mysql, utf8/utf8mb4/latin1) and use tryFrom for validation
- Api: make AbstractApi::setModule(object) lenient for test doubles (anonymous class in DispatcherTest)
- Tests: apiEndpoint() now ensures identity via getIdentity() catch and sets Admin/Client/Guest based on Api class

* Type AbstractApi module as Module

* Update Doctrine factory copyright years

* refactor(enums): relocate Client/Order status enums to domain modules

Move FOSSBilling\Enums\ClientStatusEnum -> Box\Mod\Client\Enum\Status
and FOSSBilling\Enums\ClientOrderStatusEnum -> Box\Mod\Order\Enum\Status,
colocating statuses with their owning entities (Client.php:29,
Order.php:24) and removing the generic FOSSBilling\Enums catch-all.
Drops Enum suffix, fixes ClientOrder vs Order naming mismatch, updates
sole consumer Massmailer\Service and deletes src/library/FOSSBilling/Enums/.

* refactor(exception): clean up BaseException and InformationException

- BaseException: type stackTrace(int $length,bool $protected), fix
  camelCase ($Length->$length, $my_entry->$arg, $entry_file/file),
  debug_backtrace typing, copyright 2025->2026
- InformationException: remove redundant passthrough __construct, now
  minimal marker extends BaseException (retains 100+ user-facing
  throw sites and Sentry ignore distinction)

* refactor(http): harden Http factories, fix escaping and copyrights

- CsvResponseFactory: validate table name preg_match /^[a-zA-Z0-9_]+$/ and throw
  InformationException, add import, copyright 2025->2026
- ErrorPage: escape message via htmlspecialchars in renderPage, use
  escapedMessage for fallback and original toggle, copyright 2025->2026
- ExceptionResponseFactory: split rawMessage, htmlspecialchars only for
  API_MODE Json, pass raw to ErrorPage (single escape source)
- RequestFactory: copyright 2025->2026

* Clean up locale detection error handling

* refactor(interfaces): split generic Interfaces into domain colocated contracts

- Delete FOSSBilling\Interfaces catch-all (5 files)
- FOSSBilling\Interfaces\ApiArrayInterface -> FOSSBilling\Api\ArrayInterface
  (ArrayInterface, removes Api stutter, 60+ entities)
- FOSSBilling\Interfaces\InjectionAwareInterface -> FOSSBilling\Container\InjectionAwareInterface
  (Container, 100+ services)
- FOSSBilling\Interfaces\SecurityCheckInterface -> FOSSBilling\Security\CheckInterface
  (Security, removes Security stutter, 4 checks)
- FOSSBilling\Interfaces\TimestampInterface -> FOSSBilling\Doctrine\TimestampInterface
  (Doctrine, with TimestampTrait)
- FOSSBilling\Interfaces\WidgetProviderInterface -> Box\Mod\Widgets\ProviderInterface
  (Widgets module, provider contract for Branding etc)
Update 172 usages, keep InjectionAware/Timestamp short names, shorten
ApiArray/SecurityCheck/WidgetProvider stutter.

* Type logger cache as array in factory

* Update pagination copyright headers

* Update CentralAlerts copyright year

* Clean up sanitizer class imports

* Refine security helpers and enum namespace

* Update system class copyright headers

* Move Twig AppArea enum to root namespace

* Update copyright year in update classes

* Drop legacy Arr/Str wrappers and DI binding

* refactor(utils): complete Tools split and delete god class

- Delete FOSSBilling\Tools (534 lines) god class, remove load.php require/use
- Migrate 16 call sites: di['tools']->validateAndSanitizeEmail -> EmailValidator::validateAndSanitizeEmail (8 Api), News slug -> Str::slug, generatePassword already via Credential, humanReadableBytes via Utils\Format, etc.
- Remove 20 unused use FOSSBilling\Tools imports (Order/Extension/Currency etc.)
- Harden Arr::getPairsForTableByIds with preg_match table validation and backticks (CsvResponseFactory style)
- Remove Str legacy wrappers (slugLegacy/slugInstance/to_camel_case)
- Update 11 test files to remove Tools mocks (Extension/Product/Profile/Custompages/Staff/Client/Email/Invoice/Support) and Container helper
- phpstan 443 OK (one less file), tests 60/6 pre-existing failures (AbstractApi identity/pagination) remain, Tools InvalidCount 54->0 for new code

* Update RequiredParams copyright year

* Update copyright headers to 2026

* Remove dead validation code

* fix(ci): address Rector, CS-Fixer and test failures after library reorg

- Run Rector fixes: RemoveUselessReturnTag, RemoveReadonlyOnReadonlyClass, ReadOnlyProperty, RecastingRemoval
- Run cs:fix (132 files) for import ordering and formatting
- Fix Pest helper to handle Mockery mocks for Admin/Client identities
- Fix DispatcherTest to use Module mocks for extension
- Update Client tests to remove Tools mocks and use new EmailValidator/Url
- Fix Custompages ServiceTest to use real Str::slug and long titles
- Fix Pagination mocks to use Service class
- Fix i18n, InformationException, Requirements, Config namespaces
- Add url to test container with SYSTEM_URL base
- Fix a few remaining service tests (Profile, Extension, Product, Support)

phpstan 451/451 OK, pest now 11 failures (down from 107)

* fix(tests): make suite green after reorg

- Fix Extension ServiceTest InformationException namespace and Str mock
- Fix Product Api Admin paginated products mock
- Fix Client/Service i18n, Profile i18n, and url container
- Fix Custompages long-title slug handling
- All 2998 tests pass (2 skipped), phpstan 451/451

* style: fix cs

* fix(docker): update Version.php path after move to System namespace

* fix(tests): remove remaining Tools references

- Delete obsolete ToolsTest (Tools class removed)
- Remove di['tools'] mocks from Profile ServiceTest

* feat(update): add patch115 to clean up library reorg orphans

Removes stale PSR-4 paths left on disk after refactor/library-reorganization
(Tools split, moved namespaces, low-case i18n) which persist on updated
installs because the updater extracts on top. Uses executeFileActions +
removeEmptyDirectories with exists guards, idempotent.

Bumps fresh-install last_patch to 115 to match latestPatchLevel().

* fix(cache): align TLS + instance_id merge with reorg namespaces

- Use Normalizer::normalizeBoolean instead of removed Tools
- Use BaseException + driver->value for instance_id/TLS errors
- Update tests to expect BaseException

* fix(tests): use US spelling to satisfy spellcheck

Normalise -> Normalize, normalised -> normalized

* refactor(box): move Box legacy globals to semantic namespaces

- Box_App/AppAdmin/AppClient -> FOSSBilling\Http\App/AppAdmin/AppClient
- Box_Authorization -> FOSSBilling\Security\Authorization
- Box_Event/EventDispatcher/EventManager -> FOSSBilling\Event\Event/Dispatcher/Manager
- Codemod 499 references across src/modules, src/di.php, src/index.php, tests
- Fix namespace imports (ArrayObject, Pimple\Container, ReflectionMethod, \RuntimeException, \InvalidArgumentException, \Box\Mod\*, \Override)
- Add Patcher patch116 to clean up orphaned src/library/Box/* + bump last_patch 115->116

* refactor(update): extract Patcher patches to Patch/* orchestrator

- Create FOSSBilling\Update\Patch\PatchInterface + PatchRegistry (explicit MAP, not Finder)
- Move 89 private patch25..patch116 methods to dedicated classes Patch25..Patch116
- Each Patch::apply(Patcher $patcher) delegates via public helpers ($patcher->executeSql etc.)
- Patcher now shrinks from 3624 to 1049 LOC: getPatches() uses PatchRegistry::MAP, applyCorePatches() loops [new PatchClass(), 'apply'] with call_user_func($patch, $this)
- Make helpers public (executeSql, fetchAll, tableHasColumn etc.) and properties public for patch access
- Update UpdatePatcherTest to invoke Patch*::apply and expect 'apply' method, keep ordering/skip checks
- No behavior change: MySQL guard, SchemaSynchronizer, last_patch 116 intact

* fix(spellcheck): add checkperm to dictionary after Box->Http move

* style: apply Rector StringClassNameToClassConstant + typed const

* style: apply Rector IfToNullCoalescing for changed files

* style: apply remaining Rector IfToNullCoalescing

* refactor(update): drop redundant Patch101

ADD INDEX client_order_unpaid_invoice_id_idx is already declared in
Order.php:19 and covered by SchemaSynchronizer's additive sync (safeAddedIndexes).
On PG/SQLite Patch101 never ran (isMysqlDriver guard), MySQL now gets it
via syncPortableSchema after the patch loop. Keeps last_patch at 116,
adds gap 101 alongside 92/95/98.

* refactor(update): make Patch116 superset of 115+116, keep 115 as no-op gap

* fix(spellcheck): add superset/orphans after Patch115/116 merge

* refactor(update): remove Patch115 file, keep 116 as superset

Patch115 orphan cleanup is now handled by Patch116 superset (39 paths).
115 remains a gap (like 92/95/98/101) — installs at 115 still get cleanup via
116's idempotent deletes.

* refactor(update): make Patch39 superset of 38+39, drop Patch38 file

38 (Ispconfig3/Virtualmin) + 39 (srp) are idempotent file deletions.
39 now deletes all 3 orphans; 38 remains gap like 101/115.
Installs at 37 -> 39 and 38 -> 39 both get cleanup via 39 superset.

* fix(spellcheck): add Ispconfig3 for Patch39 superset

* refactor(update): make Patch33 superset of 30-33, drop Patch30-32 files

30 (guzzlehttp) + 31 (htaccess/config.old) + 32 (phpmailer/Box/*) + 33 (FileCache)
are idempotent file deletions. 33 now deletes all 19 orphans; 30-32 remain gaps
like 38/101/115. Installs at 29 -> 33 and 32 -> 33 both get cleanup via 33 superset.

* fix(spellcheck): add idempotently/gaps/FileCache for Patch33 superset

* refactor(update): move gateway/branding helpers into Patch64

- migrateDefaultBrandingAssetsToPublicDirectory and
  migrateGatewayAssetsToPublicDirectory were only used by Patch64.
  Move them as private helpers inside Patch64 (param Patcher $patcher)
  and remove public versions from Patcher.
- Patcher shrinks by ~66 LOC, stays public only for shared helpers.

* Drop getVersion from update patches

* refactor(update): move downloadable helpers into Patch65

- generateDownloadableStoredFilename, copyLegacyDownloadableFile,
  migrateDownloadableProduct/Service/OrderStorageKeys were only used
  by Patch65 (and internally). Move them as private helpers inside
  Patch65 (private array $downloadableStorageMigrationMap + 5 methods
  taking Patcher $patcher) and remove public versions + property from
  Patcher. Keeps Patcher public API for shared helpers only.

* refactor(update): move email template helpers into Patch52

- getDefaultEmailTemplateData and getDefaultEmailTemplatePath were only
  used by Patch52 directly (and via Patcher::restoreLegacy... for 57/90).
  Move them as private helpers inside Patch52 (taking Patcher $patcher)
  so Patch52 is self-contained. Keep public versions in Patcher for
  restoreLegacyDefaultEmailTemplates used by Patch57/90 (duplicate for now,
  could also be moved if desired).

* refactor(update): move email template helpers into Patch52/57/90

- getDefaultEmailTemplateData and getDefaultEmailTemplatePath were only
  used by Patch52 direct and via Patcher::restoreLegacyDefaultEmailTemplates
  for Patch57/90. Move them as private helpers into Patch52 (self-contained)
  and duplicate into Patch57/90 alongside private
  restoreLegacyDefaultEmailTemplates (which also moved from Patcher).
- Remove public versions from Patcher — keeps Patcher public API for shared
  helpers only (executeSql, tableHasColumn etc.). Fixes unrelated
  Patcher.php diff (\s vs \s) by removing the methods entirely.

* refactor(update): move custom page slug helpers into Patch102

- allocateUniqueCustomPageSlug and fitCustomPageSlug were only used
  by Patch102 (via $patcher->). Move them as private helpers inside
  Patch102 (taking Patcher $patcher, using $patcher->fetchOne and
  $this->fitCustomPageSlug) and remove public versions from Patcher.
  Keeps Patcher public API for shared helpers only.

* refactor(update): make Patcher helpers and patch bookkeeping driver-agnostic

- applyConfigPatches: normalize driver aliases (mysql/pgsql/sqlite -> pdo_*) and use per-driver port defaults (5432 for pgsql, none for sqlite) instead of hardcoded 3306; aligns with DriverManagerFactory::DEFAULT_PORTS.
- setPatchLevel: replace MySQL-only ON DUPLICATE KEY UPDATE with portable SELECT then UPDATE/INSERT so last_patch works on pgsql/sqlite.
- Rename isMysqlDriver -> isLegacyPatchDriver (keep isMysqlDriver as deprecated alias); availablePatches/applyCorePatches now use isLegacyPatchDriver; update SchemeSynchronizer and test docblocks.
- Make tableExists/getTableColumns/getColumnLength/getColumnType/tableHasIndex portable via AbstractSchemaManager + driver-specific fallbacks (information_schema.tables WHERE table_schema='public' for pgsql, sqlite_master for sqlite); MySQL path keeps SHOW COLUMNS/INDEX for BC.
- updateTable: use driver-aware quoting (backticks for mysql, double quotes for pgsql/sqlite) instead of hardcoded backticks.
Verified: phpstan 536/536, cs:fix clean, pest 2990 passed.

* chore: add pg catalog words to spellcheck dictionary

* Remove deprecated compatibility shims

* refactor(doctrine): expand Driver enum and use it throughout

- Driver.php: add PdoPgsql/PdoSqlite cases, tryFromAlias(), defaultPort(), isMysql/isSqlite helpers; was single-case.
- DriverManagerFactory.php: use Driver::tryFromAlias() in normalizeDriver(), Driver::tryFrom()->defaultPort()/isMysql/isSqlite in getDatabaseConfig/buildConnectionParams/applySessionSettings instead of string compares.
- Patcher.php: use Driver enum in applyConfigPatches (alias + defaultPort), isLegacyPatchDriver (isMysql), tableExists/getTableColumns/getColumnLength/getColumnType/tableHasIndex/updateTable (portable via AbstractSchemaManager + enum driver checks).
Ensures single source of truth for supported drivers; phpstan 536/536, pest 2990.

* Unify HTML sanitization across Twig and APIs

Replace the old sanitizer helpers with a shared HtmlSanitizerFactory and reuse context-specific sanitizers for content, markdown, adapter output, and theme settings. This also exposes sanitize_html and sanitize_markdown Twig filters, sanitizes markdown render output and email/signature HTML consistently, and simplifies support/email input cleanup by trimming/null-byte stripping instead of HTML-encoding raw input.

* fix: align PayGateway entities with Doctrine TimestampInterface after merge

* Type HTML sanitizer scheme constants

* style: fix import order and FQCN usage

* refactor(sanitizer): centralize HtmlSanitizerFactory sanitization and remove redundant config

* fix: add commonmark to cspell dictionary

* fix: apply Rector autofixes for library reorganization

Addresses Code Quality failures after merging main:
- remove unnecessary (string)/(bool) recasts (RecastingRemovalRector)
- add strict string casts for null-able args (NullToStrictStringFuncCallArgRector)

Files are limited to the PR's changed PHP set (BASE_SHA...HEAD) so
pre-existing drift elsewhere doesn't block this branch.

* fix: stabilize DomainValidator arrow function type for Rector

Add explicit string type to array_filter callback to prevent
oscillation between NullToStrictString (add cast) and
RecastingRemoval (remove cast) on the tld parameter.

* fix: apply Rector autofixes after merge from main

BinaryOpNullableToInstanceof / WhileNullableToInstanceof

* style: fix CS imports after Rector (Invoice / ServiceLicense FQCN -> imported)

* fix: use FOSSBilling\Event\Event in Antispam isSpam test (Box_Event -> Event reorg)

* Move src/library/FOSSBilling to src/core

First-party namespaced classes now live in src/core, leaving src/library
as the legacy PSR-0 adapter layer (Payment_, Registrar_, Server_).

- composer.json: PSR-4 FOSSBilling\ -> src/core/ (classmap unchanged)
- load.php/install.php: new PATH_CORE constant; pre-autoload requires repointed
- Update/ReadinessCheck: core/ added to the writable-folder checks
- Patch117: removes the obsolete library/FOSSBilling tree on upgraded installs
- Dockerfile: copies src/core in the vendor stages; release rewrites
  (Version.php, SentryHelper.php, archive composer remap) repointed
- labeler/CODEOWNERS/cspell/AGENTS.md/test path references updated
- install/sql/content.sql: last_patch seed 116 -> 117

* Rename the FOSSBilling\ namespace to FOSSBilling\Core\

All first-party namespaced classes now live under FOSSBilling\Core\,
matching their src/core location. Clean cut, consistent with the Box\
removal on this branch: released extensions referencing the old names
must update their imports once in the same release.

- namespace/use/FQCN/docblock rewrite across src/ and tests/ (626 files)
- composer.json PSR-4: FOSSBilling\Core\ -> src/core/
- Dockerfile release composer remap updated to match
- SentryHelper: core.class tag for core paths, library.class retained
  for the PSR-0 adapter layer
- Doctrine proxy namespaces (prod + test) follow the new prefix
- tests/Unit/FOSSBilling -> tests/Unit/FOSSBilling/Core; template
  harness dirname depths adjusted
- ignore Strict/ test-harness findings artifacts

* Refresh the Composer autoloader after restoring the cached vendor tree

The php-build vendor cache is keyed on composer.lock, whose content-hash
does not change for autoload-only composer.json edits. On a cache hit the
install step is skipped entirely, leaving vendor/autoload.php generated
from the old PSR-4 mapping - so the library-to-core reorganization made
Rector fatal with "Trait FOSSBilling\Core\Doctrine\TimestampTrait not
found" while compiling analyzed files. Re-dump the autoloader from the
current composer.json after the cache restore.

* Trim comments on the two hand-written pieces to house style

Patch116 uses two dense lines with a short SHA rather than a commit
title; mirror that for Patch117. Shorten the autoloader-refresh comment
in php-quality-setup to the essentials.

* Fix two stale references from the library-to-core move

- GeoIP\Reader still built its bundled MMDB paths from PATH_LIBRARY
  . FOSSBilling - point them at PATH_CORE so the default reader works
  now that Patch117 removes the old tree; add existence tests so a
  future move can't silently break this again.
- Dispatcher passed Request::getClientIp() (nullable on CLI, e.g. cron)
  to the string-typed setIp(), TypeErroring before the first cron task;
  coalesce to '' to match AbstractApi's own empty-string default.

* Dockerfile: namespace-wrapped cache sanity check belongs under Core

* Add dasharray to cspell words (SVG stroke-dasharray in branding assets)

Fixes Spellcheck failure on the push CI run.

* Deslop merge resolutions: trim comments, drop dead test stubs, import Patch classes

* Fix stale old-namespace references found in completeness audit

- CONTRIBUTING.md documented src/library/FOSSBilling paths/namespaces
- EntityMigrationCoverageTest read the deleted
  library/FOSSBilling/UpdatePatcher.php; scan Patcher.php + Patch
  classes instead (proven to detect ported migrations)

* Satisfy Rector 2.8: drop redundant cast, add nullable return type

* Address code review: restore Patch30-32, trim Patch33, drop retired hook call in Patch55

- Patch55 called mod_service('hook')->batchConnect(), removed on main
  when the hook runtime was retired; the throw skipped the remaining
  spamchecker cleanup. Match main (no hook lines).
- Patch33 had absorbed patches 30-32 cleanups, newly deleting the live
  vendor/guzzlehttp (now required via sentry/sentry) for installs past
  patch 30. Restore Patch30/31/32 as separate classes matching main and
  trim Patch33 to the FileCache.php removal.

* Fix uninitialized Arr helper for group-restricted promos

enrichPromoApiArray built Arr without a container, so any promotion
with client-group restrictions fatally dereferenced null di in
getPairsForTableByIds. Initialize the helper with the service
container (with the file's usual missing-container guard) and add a
regression test.

* Keep client-group lookup portable: drop MySQL backticks

The Tools split introduced backtick quoting around the validated
table name; PostgreSQL rejects it. Main used the validated unquoted
name - restore that and assert the exact SQL in the regression test.
2026-11-01 22:15:41 +00:00
..