Skip to content

Code review (Floxum): chunk backfill migration, pagination caps, GET-path write, length guards, admin/TS cleanups #2

Description

@karl-bullock

Floxum code review of the referral extension. Verified every finding below against the current source — all nine hold. None block publishing; the migration one is the only thing with real downtime risk on a big forum. Fixes for each are in the comments.

Summary: backend is architecturally sound and idiomatic for Flarum 2.x (scoped container binding, concurrency-safe getOrCreateForUser fallback, denormalised referral_count to dodge N+1 on post-author lists, proper NULL handling in the unique index). Frontend is functional but plain ES5/ES6 rather than TypeScript. Two things to weigh up: the unbatched backfill UPDATE, and a DB INSERT happening on a GET serialization path.

Medium — production risk

  • Unbatched correlated-subquery UPDATE across the whole users table in migration (migrations/2026_05_27_000001_add_referral_count_to_users.php:32). The backfill runs a correlated (SELECT COUNT(*) FROM referral_invited_user WHERE …) for every users row in one statement. On a forum with tens of thousands of users this holds a write lock on users for the full count, queueing registrations/profile requests and risking timeouts during migration. Chunk it (chunk(500, …) over user IDs), or only update users that actually appear in referral_invited_user to keep the working set small.

Low — production risk

  • ListCampaignCodesController fetches all campaign codes with no pagination (src/Api/Controller/ListCampaignCodesController.php:18). whereNull('user_id')->orderBy('id','desc')->get() returns every code in one query/response. Ongoing campaigns could accumulate hundreds. Add a hard ->limit(200) (plus a total meta field) or cursor/offset pagination.
  • DB INSERT issued during GET serialization (getOrCreateForUser on read path) (src/Api/UserResourceFields.php:43). The referralCode getter may INSERT on every /api/users/me for an eligible user who's never opened their profile — a write inside a GET breaks HTTP semantics and a caching proxy could serve a 200 that still triggered an upstream write. Split generation from serialization: an explicit POST /api/referral/my-code that generates on demand, with the attribute returning null until a row exists; frontend calls it once when the referrals tab opens.

Low — robustness

  • No max-length guard on cookie-supplied invite code before DB lookup (src/Http/CaptureReferralCookieMiddleware.php:28). The raw cookie is trimmed and stored with no length cap, then handed to InviteCode::where('code', strtoupper($code))->first(). Prepared statements stop injection, but an attacker can send an arbitrarily long cookie on every unauthenticated POST /api/users with no early rejection. Valid codes are ≤8 chars — reject strlen($code) > 16 in the middleware (set code to null).
  • No max-length validation on campaign code label before insert (src/Api/Controller/CreateCampaignCodeController.php:28). Label is trimmed but unconstrained; the column is VARCHAR(255). A >255-char label throws a DB truncation/constraint error surfacing as an unhandled 500 instead of a clean 422. Add a length check throwing ValidationException(['label' => [trans('linkrobins-referral.api.label_too_long')]]).

Low — conventions

  • Admin panel registered via override on ExtensionPage rather than app.extensionData (js/admin.js:16). Patching the shared ExtensionPage.prototype.content and guarding on extension.id affects all admin extension pages. Use app.extensionData.for('linkrobins-referral').registerContent(() => m(ReferralAdminPage)) with an extracted component and drop the prototype override.
  • flarum-tsconfig devDependency present but source is plain JS (js/package.json:9). admin.js/forum.js are plain ES5/ES6, so the tsconfig dep is unused and misleading. Either migrate to TS (rename .ts, import from @flarum/core) or drop flarum-tsconfig from devDependencies.

Low — dead code / technical debt

  • joinTime attribute registered on UserModel but never used (js/forum.js:26). UserModel.prototype.joinTime = Model.attribute('joinTime', Model.transformDate) is never read, and core exposes joinedAt (not joinTime), so it always resolves to undefined. Remove the line; use user.joinedAt() if account age is ever needed.
  • Pervasive inline styles in ReferralsPage instead of LESS classes (js/forum.js:120). ReferralsPage.content() embeds layout/typography as inline style attributes (~40 lines). Inline styles can't be themed without !important and are invisible to the extension's own LESS. Extract into named classes (.ReferralProfile-code, .ReferralProfile-count) in less/forum.less, register via (new Extend\Frontend('forum'))->css(...), and swap to className.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions