Claudesbbs to main#372
Conversation
Synchronet's qnet-ftp.js STORs the reply packet in the current working directory without issuing a CWD first, so root-level .rep/.zip uploads were being rejected. FtpVirtualFilesystem::isQwkUploadFile() now also matches a bare *.rep/*.zip at path depth 1, and FtpServer routes those transfers through importUploadedRep instead of storeIncomingUpload. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Expose QWK download file at / in addition to /qwk/download/ so FTP clients see it without navigating subdirectories - Pre-build QWK packet eagerly at FTP login so directory listings report the actual file size rather than 0 - Declare ext-ftp requirement in composer.json (used by FtpTransport for outbound QWK polling); document it in docs/INSTALL.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fill in field tables for 13 endpoints that had description-only response sections (no field table): binkp/status, kept-packets, logs, logs/search, fileareas/stats, media/raw, echomail shares, and all four subscriptions endpoints - Correct binary responses (media/raw, kept-packets/bundle/download) to Response (binary) with response header tables instead of JSON - Fix pagination structure on GET /api/messages/netmail and GET /api/messages/echomail: fields were documented at the top level but the code returns them inside a pagination sub-object - Document the full netmail object shape (17 fields) on the list and conversation endpoints - Document the full echomail object shape (19 fields) on the list and conversation endpoints - Add unreadCount and threaded envelope fields to both conversation endpoints - Note replyto_address/replyto_name are absent from echomail conversation responses (kludge parsing is skipped in that path) - Fix auth labels on subscriptions endpoints (Public -> Requires authentication) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Document all remaining unexpanded object/array response fields:
- GET /api/i18n/catalog: clarify catalogs is namespace-keyed map
- GET /api/messages/netmail/{id}: document attachments[] sub-fields
- GET /api/messages/echomail/stats: fix areas field (integer not array),
add full response table with filter_counts sub-fields
- POST /api/messages/echomail/ignore-rules: add message_params.sender_name
- GET /api/messages/drafts: document drafts[] sub-fields from drafts table
- GET /api/messages/drafts/{id}: document draft{} sub-fields
- GET /api/messages/templates: document templates[] sub-fields
- GET /api/messages/templates/{id}: document template{} sub-fields
- POST /api/qwk/upload: change errors type to 'array of strings'
- GET /api/user/terminal-settings: document settings{} sub-fields
- GET /api/user/terminal-mail-state: document all 8 settings{} sub-fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CLAUDE.md: expand API route change rule to require sub-field rows for every object/array field; name the failure condition explicitly and link to DEVELOPER_GUIDE for full spec - DEVELOPER_GUIDE.md: add Response table format section under API Documentation Generator with four named rules, notation examples, and a complete sample response table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds PGP key management end-to-end (DB schema, PHP services, API routes, UI templates, JS helpers), introduces realtime listener/publisher abstractions, a database platform layer, configurable pipe-code parser mode across web/terminal, address-book PGP contact integration, new CLI/tooling and service worker updates, and extensive documentation. ChangesPGP, Realtime, DB Platform, and Parser Mode
Sequence Diagram(s)sequenceDiagram
participant BinkStream
participant PostgresEventPublisher
BinkStream->>PostgresEventPublisher: publish('binkstream', eventId)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
|
There was a problem hiding this comment.
Actionable comments posted: 18
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (14)
config/i18n/de/common.php-5007-5007 (1)
5007-5007:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix German grammar in upload progress text.
Line 5007 currently reads awkwardly (
Oeffentlichen Schluessel wird hochgeladen...). Please switch to grammatically correct phrasing (e.g.,Oeffentlicher Schluessel wird hochgeladen...).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@config/i18n/de/common.php` at line 5007, Update the translation value for the key 'ui.settings.pgp.uploading' to correct German grammar by replacing "Oeffentlichen Schluessel wird hochgeladen..." with the grammatically correct phrase, e.g. "Oeffentlicher Schluessel wird hochgeladen..."; ensure only the string value is changed and the key name 'ui.settings.pgp.uploading' remains unchanged.config/i18n/de/errors.php-92-92 (1)
92-92:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse literal umlaut character instead of digraph.
Line 92 uses "Schluessel" instead of "Schlüssel". As per coding guidelines, German i18n catalogs should use literal accented characters (ü, ö, ä, ß) rather than digraphs (ue, oe, ae, ss). The rest of the file consistently uses literal umlauts.
🔧 Proposed fix
- 'errors.pgp.private_key_required' => 'Zum Signieren ist ein verwalteter privater Schluessel erforderlich.', + 'errors.pgp.private_key_required' => 'Zum Signieren ist ein verwalteter privater Schlüssel erforderlich.',🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@config/i18n/de/errors.php` at line 92, Replace the digraph "Schluessel" in the translation value for the key 'errors.pgp.private_key_required' with the literal umlaut form "Schlüssel" so the string reads "Zum Signieren ist ein verwalteter privater Schlüssel erforderlich."; update that exact value to use the literal "ü" to match the rest of the German i18n catalog.config/i18n/de/errors.php-729-741 (1)
729-741:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winReplace all digraphs with literal umlaut characters.
Lines 729-741 use German digraphs (ue, oe, ae) instead of literal umlauts (ü, ö, ä) throughout. This violates the coding guideline for accented catalogs and is inconsistent with the rest of this file, which uses literal umlauts in strings like "ungültig", "für", "können", etc.
🔧 Proposed fix
- 'errors.pgp.load_failed' => 'PGP-Schluessel konnten nicht geladen werden.', - 'errors.pgp.public_key_required' => 'Ein oeffentlicher Schluessel ist erforderlich.', - 'errors.pgp.invalid_key' => 'Ungueltiger PGP-Schluessel.', - 'errors.pgp.save_failed' => 'PGP-Schluessel konnte nicht gespeichert werden.', - 'errors.pgp.invalid_keypair' => 'Ungueltiges PGP-Schluesselpaar.', - 'errors.pgp.key_not_found' => 'PGP-Schluessel nicht gefunden.', - 'errors.pgp.private_key_not_found' => 'Privater Schluessel nicht gefunden.', - 'errors.pgp.delete_failed' => 'PGP-Schluessel konnte nicht geloescht werden.', + 'errors.pgp.load_failed' => 'PGP-Schlüssel konnten nicht geladen werden.', + 'errors.pgp.public_key_required' => 'Ein öffentlicher Schlüssel ist erforderlich.', + 'errors.pgp.invalid_key' => 'Ungültiger PGP-Schlüssel.', + 'errors.pgp.save_failed' => 'PGP-Schlüssel konnte nicht gespeichert werden.', + 'errors.pgp.invalid_keypair' => 'Ungültiges PGP-Schlüsselpaar.', + 'errors.pgp.key_not_found' => 'PGP-Schlüssel nicht gefunden.', + 'errors.pgp.private_key_not_found' => 'Privater Schlüssel nicht gefunden.', + 'errors.pgp.delete_failed' => 'PGP-Schlüssel konnte nicht gelöscht werden.', 'errors.pgp.disabled' => 'PGP ist auf diesem System deaktiviert.', - 'errors.pgp.managed_disabled' => 'Die Erzeugung verwalteter PGP-Schluessel ist auf diesem System deaktiviert.', - 'errors.pgp.passphrase_too_short' => 'Verwenden Sie eine laengere PGP-Passphrase.', - 'errors.pgp.passphrase_mismatch' => 'Die Passphrase-Bestaetigung stimmt nicht ueberein.', - 'errors.pgp.generation_failed' => 'BBS-verwalteter PGP-Schluessel konnte nicht erzeugt werden.', + 'errors.pgp.managed_disabled' => 'Die Erzeugung verwalteter PGP-Schlüssel ist auf diesem System deaktiviert.', + 'errors.pgp.passphrase_too_short' => 'Verwenden Sie eine längere PGP-Passphrase.', + 'errors.pgp.passphrase_mismatch' => 'Die Passphrase-Bestätigung stimmt nicht überein.', + 'errors.pgp.generation_failed' => 'BBS-verwalteter PGP-Schlüssel konnte nicht erzeugt werden.',As per coding guidelines, when editing German or other accented catalogs, use literal characters only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@config/i18n/de/errors.php` around lines 729 - 741, Edit the German error strings for the PGP keys block (e.g., keys like 'errors.pgp.load_failed', 'errors.pgp.public_key_required', 'errors.pgp.invalid_key', 'errors.pgp.save_failed', 'errors.pgp.invalid_keypair', 'errors.pgp.key_not_found', 'errors.pgp.private_key_not_found', 'errors.pgp.delete_failed', 'errors.pgp.disabled', 'errors.pgp.managed_disabled', 'errors.pgp.passphrase_too_short', 'errors.pgp.passphrase_mismatch', 'errors.pgp.generation_failed') and replace all digraphs (ae, oe, ue, ss where appropriate) with the corresponding literal German characters (ä, ö, ü, ß) so the messages use proper umlauts and match the rest of the file.config/i18n/es/errors.php-728-738 (1)
728-738:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix missing Spanish accents in error messages.
Several Spanish error messages are missing required accent marks (tildes). According to the coding guidelines, accented catalogs must use literal accented characters.
✏️ Proposed fix
- 'errors.pgp.public_key_required' => 'Se requiere una clave publica.', + 'errors.pgp.public_key_required' => 'Se requiere una clave pública.', - 'errors.pgp.invalid_key' => 'Clave publica PGP invalida.', + 'errors.pgp.invalid_key' => 'Clave pública PGP inválida.', - 'errors.pgp.invalid_keypair' => 'Par de claves PGP invalido.', + 'errors.pgp.invalid_keypair' => 'Par de claves PGP inválido.', - 'errors.pgp.key_not_found' => 'No se encontro la clave PGP.', + 'errors.pgp.key_not_found' => 'No se encontró la clave PGP.', - 'errors.pgp.private_key_not_found' => 'No se encontro la clave privada.', + 'errors.pgp.private_key_not_found' => 'No se encontró la clave privada.', - 'errors.pgp.disabled' => 'PGP esta desactivado en este sistema.', + 'errors.pgp.disabled' => 'PGP está desactivado en este sistema.', - 'errors.pgp.managed_disabled' => 'La generacion de claves PGP gestionadas esta desactivada en este sistema.', + 'errors.pgp.managed_disabled' => 'La generación de claves PGP gestionadas está desactivada en este sistema.', - 'errors.pgp.passphrase_too_short' => 'Use una frase de acceso PGP mas larga.', + 'errors.pgp.passphrase_too_short' => 'Use una frase de acceso PGP más larga.', - 'errors.pgp.passphrase_mismatch' => 'La confirmacion de la frase de acceso no coincide.', + 'errors.pgp.passphrase_mismatch' => 'La confirmación de la frase de acceso no coincide.',As per coding guidelines: "When editing French or other accented catalogs, use literal characters (e.g., 'é', 'à') only."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@config/i18n/es/errors.php` around lines 728 - 738, The Spanish error strings for the PGP keys are missing proper accents; update the translation values for the listed keys (errors.pgp.public_key_required, errors.pgp.invalid_key, errors.pgp.save_failed, errors.pgp.invalid_keypair, errors.pgp.key_not_found, errors.pgp.private_key_not_found, errors.pgp.delete_failed, errors.pgp.disabled, errors.pgp.managed_disabled, errors.pgp.passphrase_too_short, errors.pgp.passphrase_mismatch) to use literal accented characters (e.g., "clave pública", "inválida", "no se encontró", "está", "generación", "más", "confirmación") so each message uses correct Spanish accents per the catalog guidelines.config/i18n/it/common.php-1814-1814 (1)
1814-1814:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix Italian accenting in user-facing copy.
Line 1814 should use
èin “Quando è abilitato” to avoid awkward/incorrect Italian in the admin UI.As per coding guidelines: “When editing French or other accented catalogs, use literal characters (e.g., 'é', 'à') only.”Suggested fix
- 'ui.admin.bbs_settings.features.pgp_managed_keys_help' => 'Quando e abilitato, gli utenti possono generare coppie di chiavi PGP nel browser e salvare su questo server il blob cifrato della chiave privata.', + 'ui.admin.bbs_settings.features.pgp_managed_keys_help' => 'Quando è abilitato, gli utenti possono generare coppie di chiavi PGP nel browser e salvare su questo server il blob cifrato della chiave privata.',🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@config/i18n/it/common.php` at line 1814, Update the Italian translation string for the key 'ui.admin.bbs_settings.features.pgp_managed_keys_help' by replacing the unaccented "Quando e abilitato" with the correct accented phrase "Quando è abilitato"; ensure the literal Italian accented character è is used in the PHP array value so the admin UI displays correct Italian.config/i18n/it/common.php-4955-5041 (1)
4955-5041:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winNormalize accenting across the new Italian PGP strings.
Several new values in this block drop accents (
è,così,verrà, etc.), which makes the new PGP UI text look inconsistent with the rest of the locale.As per coding guidelines: “When editing French or other accented catalogs, use literal characters (e.g., 'é', 'à') only.”Representative fixes
- 'ui.settings.pgp.primary_help' => 'Scegli qui una chiave, oppure usa il pulsante nella tabella sotto per impostarla come principale, cosi cambi quale chiave pubblica viene mostrata come preferita sul keyserver.', + 'ui.settings.pgp.primary_help' => 'Scegli qui una chiave, oppure usa il pulsante nella tabella sotto per impostarla come principale, così cambi quale chiave pubblica viene mostrata come preferita sul keyserver.', - 'ui.settings.pgp.openpgp_missing' => 'OpenPGP.js non e ancora installato su questo BBS, quindi la generazione nel browser non e disponibile.', + 'ui.settings.pgp.openpgp_missing' => 'OpenPGP.js non è ancora installato su questo BBS, quindi la generazione nel browser non è disponibile.', - 'ui.compose.pgp.encrypt_recipient_help' => 'Scegli la chiave pubblica che verra usata per cifrare questo netmail.', + 'ui.compose.pgp.encrypt_recipient_help' => 'Scegli la chiave pubblica che verrà usata per cifrare questo netmail.',🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@config/i18n/it/common.php` around lines 4955 - 5041, Normalize missing Italian accents in the new PGP strings: update 'ui.settings.pgp.openpgp_missing' to "non è ancora ... non è disponibile", 'ui.settings.pgp.primary_help' to use "così", 'ui.compose.pgp.encrypt_recipient_help' to use "verrà", 'ui.pgp.decrypt_help' to use "è", and 'ui.settings.pgp.managed_disabled_notice' to use "è disabilitato"; scan the other keys in this block (e.g., any string containing " e ", "cosi", "verra", "ancora") and replace plain "e"/"ancora"/"cosi"/"verra" with the proper accented forms (è, è ancora, così, verrà) to make accents consistent across the locale.config/i18n/it/errors.php-92-92 (1)
92-92:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winMissing accent in Italian text.
The verb "è" (is) requires an accent. The string should read
'Per firmare è necessaria una chiave privata gestita.'🔧 Proposed fix
- 'errors.pgp.private_key_required' => 'Per firmare e necessaria una chiave privata gestita.', + 'errors.pgp.private_key_required' => 'Per firmare è necessaria una chiave privata gestita.',🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@config/i18n/it/errors.php` at line 92, Update the translation string for the key 'errors.pgp.private_key_required' to include the missing accent on the verb "è" so the message reads "Per firmare è necessaria una chiave privata gestita."; locate the entry for 'errors.pgp.private_key_required' and replace the current value accordingly.config/i18n/it/errors.php-735-737 (1)
735-737:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winMissing accents in Italian text.
Multiple strings are missing required accents:
- Line 735: "è" (is) requires an accent
- Line 736: "è" (is) requires an accent
- Line 737: "più" (more) requires an accent
🔧 Proposed fix
- 'errors.pgp.disabled' => 'PGP e disabilitato su questo sistema.', - 'errors.pgp.managed_disabled' => 'La generazione di chiavi PGP gestite e disabilitata su questo sistema.', - 'errors.pgp.passphrase_too_short' => 'Usa una passphrase PGP piu lunga.', + 'errors.pgp.disabled' => 'PGP è disabilitato su questo sistema.', + 'errors.pgp.managed_disabled' => 'La generazione di chiavi PGP gestite è disabilitata su questo sistema.', + 'errors.pgp.passphrase_too_short' => 'Usa una passphrase PGP più lunga.',As per coding guidelines: "When editing French or other accented catalogs, use literal characters (e.g., 'é', 'à') only."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@config/i18n/it/errors.php` around lines 735 - 737, Update the three Italian translation strings to include the missing accents: change the value for 'errors.pgp.disabled' to use "è", update 'errors.pgp.managed_disabled' to use "è" in "è disabilitata", and update 'errors.pgp.passphrase_too_short' to use "più" with the accent; edit the string values for keys 'errors.pgp.disabled', 'errors.pgp.managed_disabled', and 'errors.pgp.passphrase_too_short' in config/i18n/it/errors.php to use the literal accented characters.public_html/js/netmail.js-1219-1223 (1)
1219-1223:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winHardcoded English string violates i18n guidelines.
Line 1221 uses a raw English string
"PGP encrypted"instead of an i18n key. All other PGP status strings in this function correctly useuiT().Proposed fix
if (!window.pgpManagedKeysEnabled) { - container.innerHTML = '<span class="badge bg-secondary">PGP encrypted</span>'; + container.innerHTML = '<span class="badge bg-secondary">' + uiT('ui.pgp.encrypted', 'PGP encrypted') + '</span>'; return; }Also add the key
ui.pgp.encryptedto all locale files underconfig/i18n/*/common.php.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@public_html/js/netmail.js` around lines 1219 - 1223, Replace the hardcoded English label in the PGP encryption branch with the i18n helper and add the new key to locales: in the block guarded by window.PgpMessageSupport.isEncryptedMessage(body) (the code that sets container.innerHTML currently to '<span class="badge bg-secondary">PGP encrypted</span>'), use the uiT('ui.pgp.encrypted') translation inside the same badge markup instead of the raw string; then add the key "ui.pgp.encrypted" with the appropriate translated text to every config/i18n/*/common.php locale file so all languages show the localized label.templates/compose.twig-2042-2045 (1)
2042-2045:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winInitialization condition may be too restrictive for netmail encryption.
The condition
pgpEnabled && pgpManagedKeysEnabledprevents PGP control initialization when onlypgpis enabled butpgp_managed_keysis disabled. However,initPgpComposeControls()(lines 993-994) shows that netmail encryption (canEncryptInCompose) only requires the recipient's public key — it doesn't need managed keys.Consider relaxing the condition to initialize PGP controls whenever
pgpEnabledis true:Proposed fix
- if (pgpEnabled && pgpManagedKeysEnabled) { + if (pgpEnabled) { bindPgpRecipientControls(); initPgpComposeControls(); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@templates/compose.twig` around lines 2042 - 2045, The current guard only initializes PGP UI when both pgpEnabled and pgpManagedKeysEnabled are true, which skips initPgpComposeControls/bindPgpRecipientControls for cases where pgp is enabled but managed keys are disabled; change the condition to require only pgpEnabled so that initPgpComposeControls() (and bindPgpRecipientControls() if appropriate) run whenever pgpEnabled is true, ensuring netmail encryption logic that relies on recipient public keys (canEncryptInCompose) is active even without managed keys.scripts/pgp.lookup.php-166-177 (1)
166-177:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winThis tool does not currently reproduce the full per-user compose lookup path.
PgpLookupServiceonly checks saved correspondent keys when auserIdis supplied, but the script always calls it without user context. That means a logged-in user's compose flow can resolve a different key than this diagnostic tool. Either add a--user/--user-idoption or narrow the docs so this only claims parity for local-vs-remote keyserver resolution.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/pgp.lookup.php` around lines 166 - 177, The script always calls PgpLookupService without user context so it can't reproduce per-user compose lookups; update parseArgs to accept a --user or --user-id flag, pass that userId into describeLookupTarget, findPublicKeyForDestination and searchPublicKeysForDestination (or into the PgpLookupService constructor) so the same per-user correspondent-key checks run as in compose, and ensure the args['user'] (or args['user_id']) is threaded through when calling these methods; if adding the flag is infeasible, restrict the script's docs/comments to state it does not perform per-user lookup parity instead.docs/TerminalServer.md-75-76 (1)
75-76:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winDocument all three parser modes for completeness.
Lines 75-76 mention only
decimal_relaxedandstrict, butdocs/TerminalServerDevGuide.md(lines 7-11) anddocs/UPGRADING_1.9.8.md(lines 114-118) both list three modes includingloose. For consistency and completeness, this section should briefly mention all three supported modes.📝 Suggested fix to include all modes
-Pipe-code rendering for plain bulletins and other ANSI/pipe text shared with the web renderer is controlled by the `.env` setting `PIPE_CODE_PARSER_MODE`. The default is `decimal_relaxed`, which favors common BBS content where decimal pipe codes are immediately followed by uppercase text. `strict` remains available when avoiding false positives in prose matters more. +Pipe-code rendering for plain bulletins and other ANSI/pipe text shared with the web renderer is controlled by the `.env` setting `PIPE_CODE_PARSER_MODE`. The default is `decimal_relaxed`, which favors common BBS content where decimal pipe codes are immediately followed by uppercase text. `strict` remains available when avoiding false positives in prose matters more. A third mode, `loose`, restores broader legacy matching for testing and comparison.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/TerminalServer.md` around lines 75 - 76, Update the PIPE_CODE_PARSER_MODE documentation to list all three supported modes—decimal_relaxed, strict, and loose—so it matches other docs; briefly describe each mode (e.g., decimal_relaxed: default, favors BBS content where decimal pipe codes are immediately followed by uppercase text; strict: avoids false positives in prose; loose: most permissive, catches more pipe-code variants) and ensure the setting name `PIPE_CODE_PARSER_MODE` and the three mode names are mentioned exactly as identifiers so readers can map them to the .env option.src/PgpKeyService.php-228-238 (1)
228-238:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winPreserve leading zeros when normalizing PGP fingerprint search input (strip only an optional
0x/0Xprefix).
strtoupper(ltrim($search, '0x'))removes all leading0characters (and only lowercasex), so fingerprints that legitimately start with0won’t match, and an uppercase0Xprefix won’t be removed either. Update normalization insrc/PgpKeyService.php(around lines 228-239 and 259-271).Suggested fix
- $normalizedFingerprint = strtoupper(ltrim($search, '0x')); + $normalizedFingerprint = strtoupper((string) preg_replace('/^0x/i', '', $search)); … - $normalizedFingerprint = strtoupper(ltrim($search, '0x')); + $normalizedFingerprint = strtoupper((string) preg_replace('/^0x/i', '', $search));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/PgpKeyService.php` around lines 228 - 238, The current normalization using strtoupper(ltrim($search, '0x')) incorrectly strips all leading zeros and only removes lowercase 'x'; change it to remove only a single optional "0x" or "0X" prefix and then uppercase the remainder so leading zeros are preserved—e.g., detect and strip a single leading 0x/0X (or use a regex like ^0[xX]) on the $search value before calling strtoupper, assign that to $normalizedFingerprint where used in PgpKeyService (the block building $sql and $params and the similar normalization later around the other referenced block), and keep the rest of the LIKE params intact.src/PgpContactKeyService.php-139-153 (1)
139-153:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winPreserve leading zeroes when normalizing fingerprint searches.
ltrim($search, '0x')removes all leading0andxcharacters, so a valid fingerprint like0x00AB...becomesAB..., breaking thek.fingerprint = ?exact-match path. Store/search fingerprints are derived/stored as 40-char SHA1 hex (leading zeros possible), so stripping them is incorrect.Suggested fix
- $normalizedFingerprint = strtoupper(ltrim($search, '0x')); + $normalizedFingerprint = strtoupper((string) preg_replace('/^0x/i', '', $search));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/PgpContactKeyService.php` around lines 139 - 153, The code strips all leading '0' and 'x' chars via ltrim($search, '0x'), which removes significant leading zeros from fingerprints; instead detect and strip only a single optional "0x" or "0X" prefix (e.g. if (stripos($search, '0x') === 0) $normalizedFingerprint = substr($search, 2); else $normalizedFingerprint = $search), then uppercase that value for the k.fingerprint exact match. Update the use of $normalizedFingerprint and the params pushed into array_push($params, ...) so the exact-match param still receives the correctly normalized 40-char hex string, and leave the wildcard $like/$exact behavior for the other LIKE clauses unchanged (refer to $normalizedFingerprint, ltrim($search, '0x'), the WHERE block and array_push call to locate the change).
🧹 Nitpick comments (11)
public_html/js/pgp-message.js (1)
450-467: 💤 Low valueUnused
signerSearchparameter.The
signerSearchparameter is declared but never used in the function body. The function always usesgetCurrentUserPrivateKeyInfo()which selects the preferred key regardless of any search criteria.If this parameter is intended for future key selection by search criteria, consider adding a TODO comment. Otherwise, remove it to avoid confusion.
Option 1: Remove unused parameter
- async function signCleartextMessage(text, passphrase, signerSearch) { + async function signCleartextMessage(text, passphrase) {Update the export and any callers accordingly.
Option 2: Document for future use
+ // TODO: signerSearch is reserved for future key selection by fingerprint/user ID async function signCleartextMessage(text, passphrase, signerSearch) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@public_html/js/pgp-message.js` around lines 450 - 467, The signerSearch parameter in signCleartextMessage is unused — remove signerSearch from the function signature and update any exports/callers to stop passing it; keep the body using getCurrentUserPrivateKeyInfo() and getCurrentUserDecryptedPrivateKey(passphrase) as-is. If you intend to support alternate key selection later, instead leave signerSearch out of runtime flow but add a single-line TODO inside signCleartextMessage referencing signerSearch and noting it will be used for alternative key lookup (e.g., via getCurrentUserPrivateKeyInfo) when implemented.src/Realtime/EventListenerInterface.php (3)
15-20: ⚡ Quick winConsider documenting timeout behavior.
The
wait()method doesn't specify what is returned when the timeout expires with no notifications. Consider adding a note that an empty array is returned on timeout.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Realtime/EventListenerInterface.php` around lines 15 - 20, The docblock for the wait(int $timeoutMs): array method is missing behavior on timeouts; update the PHPDoc for EventListenerInterface::wait to state that it returns an empty array when the timeout expires with no notifications (i.e., no payloads), so callers know to expect list<string> and can distinguish no-results vs blocking; reference the wait method name in the comment and include the timeoutMs parameter description.
10-13: ⚡ Quick winConsider documenting the return value and edge cases.
The
listen()method returnsboolbut doesn't explain whattrueorfalsesignifies. Consider documenting:
- What the return value indicates (success/failure?)
- Behavior when already subscribed to a channel
- Any exceptions that might be thrown
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Realtime/EventListenerInterface.php` around lines 10 - 13, Update the docblock for EventListenerInterface::listen to explicitly describe what the boolean return value means (e.g., true on successful subscription, false on failure), document behavior when attempting to subscribe to an already-subscribed channel (idempotent and returns true / or returns false — choose and state it), and list any exceptions the implementation may throw (e.g., TransportException, InvalidArgumentException) or state that none are thrown; ensure the comment mentions side effects (registering handlers) and any preconditions for the channel string so implementers of listen() know expected behavior.
27-30: ⚡ Quick winConsider clarifying state management and return value.
The
reconnect()documentation mentions "the active channel" which implies internal state tracking, but the interface doesn't expose methods to query this state. Consider:
- Documenting that implementations must track the last subscribed channel
- Explaining what the return value indicates
- Noting behavior if no channel was previously subscribed
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Realtime/EventListenerInterface.php` around lines 27 - 30, Update the PHPDoc for the EventListenerInterface::reconnect() method to explicitly state how implementations must manage subscription state and what the boolean return value means: document that implementations are expected to persist the "last subscribed channel" (or return false/throw if none exists), define return true on successful reconnect-and-resubscribe and false on failure (including the case where no prior channel was present), and describe any expected side effects (e.g., re-subscribe attempt, error handling). Also mention the behavior when there is no previously subscribed channel (e.g., no-op and return false or throw) so implementers know the contract.src/Realtime/BinkStream.php (1)
10-20: ⚡ Quick winExpand phpDoc to include parameter and return type documentation.
The method signature now has six parameters and returns a nullable int. Complete phpDoc improves IDE support and maintainability.
📝 Proposed phpDoc expansion
/** * Insert a realtime event row and wake listeners through the configured publisher. + * + * `@param` PDO $db Database connection + * `@param` string $eventType Event type identifier + * `@param` array $payload Event payload data + * `@param` int|null $userId Target user ID, or null for broadcast + * `@param` bool $adminOnly Whether event is admin-only + * `@param` EventPublisherInterface|null $publisher Event publisher, defaults to PostgresEventPublisher + * `@return` int|null The inserted event ID, or null on failure */ public static function emit(🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Realtime/BinkStream.php` around lines 10 - 20, Update the phpDoc for the BinkStream::emit method to document all parameters and the return value: add `@param` tags for PDO $db, string $eventType, array $payload, ?int $userId, bool $adminOnly, and ?EventPublisherInterface $publisher, and add an `@return` int|null describing the nullable integer ID returned (or null on failure). Also include a brief description of what the method does and note any side effects (e.g., inserts a realtime event row and wakes listeners via the publisher) to improve IDE hints and maintainability.templates/shells/web/base.twig (1)
278-280: ⚡ Quick winKeyserver label i18n OK; re-check bbs-menu parity
ui.settings.pgp.headingexists in all locale catalogs present (config/i18n/{de,en,es,fr,it}/common.php).- No Keyserver/PGP link exists anywhere under
templates/shells/bbs-menu/*; if the bbs-menu main menu should mirror the web menu, add the Keyserver item there using the same translation key.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@templates/shells/web/base.twig` around lines 278 - 280, The Keyserver/PGP menu item is present in web/base.twig but missing from the bbs-menu templates; update the templates under the bbs-menu (the menu partial(s) that build the main BBS menu) to add the same conditional item guarded by bbs_feature_enabled('pgp') and using the same translation key ui.settings.pgp.heading (so it mirrors the web menu); ensure the inserted item matches the markup/placement of the other menu entries and uses the same href (/keyserver) and icon class (fas fa-key).telnet/src/BulletinsHandler.php (1)
254-267: 💤 Low valueConsider caching the parser mode to avoid repeated environment reads.
getPipeCodeParserMode()is called at least twice per bulletin (lines 208, 223, and transitively at 262). Since the mode won't change during a single request, consider reading it once in the constructor or caching it in a private field.♻️ Optional refactor
class BulletinsHandler { private BbsSession $server; private string $apiBase; + private string $pipeCodeParserMode; public function __construct(BbsSession $server, string $apiBase) { $this->server = $server; $this->apiBase = $apiBase; + $mode = strtolower(trim((string)Config::env('PIPE_CODE_PARSER_MODE', 'decimal_relaxed'))); + $this->pipeCodeParserMode = in_array($mode, ['strict', 'decimal_relaxed', 'loose'], true) ? $mode : 'decimal_relaxed'; }Then replace
$this->getPipeCodeParserMode()calls with$this->pipeCodeParserMode.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@telnet/src/BulletinsHandler.php` around lines 254 - 267, Cache the parsed pipe-code parser mode in a private property to avoid repeated environment reads: add a private property (e.g., $pipeCodeParserMode), initialize it once (preferably in the constructor) by calling the existing getPipeCodeParserMode logic or by moving the mode-detection code there, and then replace all calls to $this->getPipeCodeParserMode() (used in getPipeCodeColorPattern and elsewhere) with the cached $this->pipeCodeParserMode so subsequent accesses use the stored value for the lifetime of the handler instance.scripts/database_maintenance.php (1)
267-295: 💤 Low valueConsider clarifying the dry-run message.
Line 287 says "Would evaluate ... for stale-process cleanup" but the actual operation marks rows as
failed. Consider rewording to "Would mark X stale active BinkP sessions as failed" for clarity.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/database_maintenance.php` around lines 267 - 295, Update the dry-run echo to accurately reflect the real action: instead of "Would evaluate ... for stale-process cleanup" print a message saying "Would mark X stale active BinkP sessions as failed" (use the same $result['count'] value). Locate the dry-run branch around the binkp_session_log check where $stmt/$result are used and ensure the wording mirrors the non-dry-run branch that calls SessionLogger::cleanupStaleActiveSessions and updates $totalCleaned.templates/settings.twig (3)
1371-1373: 💤 Low valueConsider logging error details for debugging.
The catch block discards the actual error, making it difficult to diagnose key generation failures. Consider logging the error to console for debugging purposes.
Suggested improvement
} catch (error) { + console.error('PGP key generation failed:', error); setPgpStatus('danger', uiT('errors.pgp.generation_failed', 'Failed to generate managed PGP key.')); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@templates/settings.twig` around lines 1371 - 1373, The catch block that calls setPgpStatus('danger', uiT('errors.pgp.generation_failed', 'Failed to generate managed PGP key.')) swallows the error; update the catch to also log the caught error (e.g., console.error or the app logger) so the actual error object/message is recorded for debugging while keeping the existing user-facing setPgpStatus call; reference the same catch handling around the PGP generation code and add a console.error(error) (or logger.error) line there.
697-774: 💤 Low valuePGP modal rendered outside the conditional block.
The
pgpKeyDetailsModalis rendered unconditionally (after line 695 where{% endif %}closes thepgp_enabledcheck), so it will be included in the page even when PGP is disabled. Consider wrapping it in the same conditional to avoid rendering unused markup.Suggested fix
</div> {% endif %} + {% if pgp_enabled %} <div class="modal fade" id="pgpKeyDetailsModal" tabindex="-1" aria-hidden="true"> ... </div> + {% endif %} {# ------------------------------------------------------------------ #}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@templates/settings.twig` around lines 697 - 774, The PGP details modal (element id "pgpKeyDetailsModal" and buttons calling copyPgpPublicKeyFromDetails()/downloadPgpPublicKeyFromDetails()) is rendered outside the pgp_enabled conditional; wrap the entire modal markup inside the same {% if pgp_enabled %} ... {% endif %} (or add a new {% if pgp_enabled %} before and {% endif %} after the modal) so the modal is only included when pgp_enabled is true.
2222-2224: 💤 Low value
escapeJsmay be incomplete for arbitrary strings.The function only escapes backslashes and single quotes. For hex fingerprints this is sufficient, but if reused for other user input, it could miss newlines (
\n), carriage returns (\r), or other control characters that break JS string literals. Since it's currently only used with fingerprints, this is acceptable but worth noting if the function is reused elsewhere.More robust escapeJs (optional)
function escapeJs(text) { - return String(text).replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + return String(text) + .replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r'); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@templates/settings.twig` around lines 2222 - 2224, The escapeJs function only escapes backslashes and single quotes which can allow control characters (e.g., newline \n, carriage return \r, tab \t) or other quotes to break JS string literals; update escapeJs to also replace control characters and other problematic characters (at minimum: \n, \r, \t, double quotes ") and any other common JS-breaking control chars so it produces a safe single-quoted JS string; modify the existing function escapeJs to perform these additional replacements while preserving current backslash and single-quote handling.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: eaa291d8-328f-4c5f-964b-609b1a256974
⛔ Files ignored due to path filters (1)
public_html/vendor/openpgp-6.2.2/openpgp.min.jsis excluded by!**/*.min.js
📒 Files selected for processing (80)
CLAUDE.mdREADME.mdconfig/bbs.json.exampleconfig/i18n/de/common.phpconfig/i18n/de/errors.phpconfig/i18n/en/common.phpconfig/i18n/en/errors.phpconfig/i18n/es/common.phpconfig/i18n/es/errors.phpconfig/i18n/fr/common.phpconfig/i18n/fr/errors.phpconfig/i18n/it/common.phpconfig/i18n/it/errors.phpdatabase/migrations/v20260602033958_add_user_pgp_keys.sqldatabase/migrations/v20260602191017_add_pgp_contact_keys.sqldatabase/migrations/v20260603181626_increase_user_settings_theme_length.sqldocs/ANSI_Support.mddocs/API.mddocs/CLI.mddocs/CONFIGURATION.mddocs/ConfigurationSystem.mddocs/DEVELOPER_GUIDE.mddocs/INSTALL.mddocs/PGP.mddocs/Pipe_Code_Support.mddocs/PostgreSQLDependencies.mddocs/TerminalServer.mddocs/TerminalServerDevGuide.mddocs/UPGRADING_1.9.8.mddocs/index.mdpublic_html/js/ansisys.jspublic_html/js/echomail.jspublic_html/js/netmail.jspublic_html/js/pgp-message.jspublic_html/sw.jspublic_html/webdoors/mrc/index.phproutes/admin-routes.phproutes/api-routes.phproutes/web-routes.phpscripts/ai_bot_daemon.phpscripts/database_maintenance.phpscripts/install.phpscripts/pgp.lookup.phpsrc/AddressBookController.phpsrc/Admin/AdminDaemonServer.phpsrc/Auth.phpsrc/BbsConfig.phpsrc/Binkp/Protocol/BinkpClient.phpsrc/Binkp/Protocol/BinkpServer.phpsrc/Binkp/SessionLogger.phpsrc/Config.phpsrc/Crashmail/CrashmailService.phpsrc/Database.phpsrc/DatabasePlatform/DatabasePlatformFactory.phpsrc/DatabasePlatform/DatabasePlatformInterface.phpsrc/DatabasePlatform/PostgresPlatform.phpsrc/MessageHandler.phpsrc/Pgp/ArmoredPublicKeyParser.phpsrc/PgpContactKeyService.phpsrc/PgpKeyService.phpsrc/PgpLookupService.phpsrc/Realtime/BinkStream.phpsrc/Realtime/EventListenerInterface.phpsrc/Realtime/EventPublisherInterface.phpsrc/Realtime/PostgresEventListener.phpsrc/Realtime/PostgresEventPublisher.phpsrc/Realtime/PostgresSseEventMaintenance.phpsrc/Realtime/SseEventMaintenanceInterface.phpsrc/Template.phptelnet/src/BulletinsHandler.phptemplates/admin/bbs_settings.twigtemplates/base.twigtemplates/compose.twigtemplates/dashboard.twigtemplates/echomail.twigtemplates/keyserver.twigtemplates/netmail.twigtemplates/settings.twigtemplates/shells/bbs-menu/base.twigtemplates/shells/web/base.twig
| 'ui.settings.tab.pgp' => 'PGP', | ||
| 'ui.settings.pgp.heading' => 'PGP-Schluessel', | ||
| 'ui.settings.pgp.help' => 'Oeffentliche Schluessel hochladen, BBS-verwaltete private Schluessel erzeugen und den bevorzugten oeffentlichen Schluessel fuer den Keyserver auswaehlen.', | ||
| 'ui.settings.pgp.upload_heading' => 'Oeffentlichen Schluessel hochladen', | ||
| 'ui.settings.pgp.key_label' => 'Schluesselbezeichnung', | ||
| 'ui.settings.pgp.key_label_placeholder' => 'Laptop-Schluessel, Arbeitsschluessel, Archivschluessel', | ||
| 'ui.settings.pgp.public_key' => 'ASCII-armored oeffentlicher Schluessel', | ||
| 'ui.settings.pgp.public_key_placeholder' => '-----BEGIN PGP PUBLIC KEY BLOCK-----', | ||
| 'ui.settings.pgp.upload_button' => 'Oeffentlichen Schluessel hochladen', | ||
| 'ui.settings.pgp.generate_heading' => 'BBS-verwalteten Schluessel erzeugen', | ||
| 'ui.settings.pgp.managed_label_placeholder' => 'BinktermPHP verwalteter Schluessel', | ||
| 'ui.settings.pgp.passphrase' => 'PGP-Passphrase', | ||
| 'ui.settings.pgp.passphrase_placeholder' => 'PGP-Passphrase eingeben', | ||
| 'ui.settings.pgp.passphrase_help' => 'Verwenden Sie eine Passphrase, die sich von Ihrem Login-Passwort unterscheidet.', | ||
| 'ui.settings.pgp.passphrase_confirm' => 'Passphrase bestaetigen', | ||
| 'ui.settings.pgp.passphrase_confirm_placeholder' => 'PGP-Passphrase erneut eingeben', | ||
| 'ui.settings.pgp.generate_button' => 'Verwalteten Schluessel erzeugen', | ||
| 'ui.settings.pgp.generate_help' => 'Dieses erzeugt das Schluesselpaar im Browser und speichert nur den verschluesselten privaten Schluessel auf dem Server.', | ||
| 'ui.settings.pgp.managed_disabled_notice' => 'Diese BBS erlaubt das Hochladen oeffentlicher PGP-Schluessel, aber das Hosten BBS-verwalteter privater Schluessel ist vom Sysop deaktiviert.', | ||
| 'ui.settings.pgp.saved_keys_heading' => 'Gespeicherte Schluessel', | ||
| 'ui.settings.pgp.keyserver_button' => 'Keyserver durchsuchen', | ||
| 'ui.settings.pgp.no_keys' => 'Noch keine PGP-Schluessel gespeichert.', | ||
| 'ui.settings.pgp.col_primary' => 'Status', | ||
| 'ui.settings.pgp.col_label' => 'Bezeichnung', | ||
| 'ui.settings.pgp.col_fingerprint' => 'Fingerprint', | ||
| 'ui.settings.pgp.col_source' => 'Quelle', | ||
| 'ui.settings.pgp.col_created' => 'Erstellt', | ||
| 'ui.settings.pgp.col_actions' => 'Aktionen', | ||
| 'ui.settings.pgp.source_managed' => 'BBS verwaltet', | ||
| 'ui.settings.pgp.source_uploaded' => 'Hochgeladen', | ||
| 'ui.settings.pgp.private_key_available' => 'Verschluesselter privater Schluessel gespeichert', | ||
| 'ui.settings.pgp.primary_selector' => 'Bevorzugter oeffentlicher Schluessel', | ||
| 'ui.settings.pgp.primary_help' => 'Waehle hier einen Schluessel aus oder nutze die Schaltflaeche "Als primaer festlegen" in der Tabelle unten, um zu aendern, welcher oeffentliche Schluessel auf dem Keyserver bevorzugt angezeigt wird.', | ||
| 'ui.settings.pgp.primary_selected' => 'Primaer', | ||
| 'ui.settings.pgp.secondary_key' => 'Sekundaer', | ||
| 'ui.settings.pgp.make_primary_button' => 'Als primaer festlegen', | ||
| 'ui.settings.pgp.current_primary_button' => 'Aktuell primaer', | ||
| 'ui.settings.pgp.uploading' => 'Oeffentlichen Schluessel wird hochgeladen...', | ||
| 'ui.settings.pgp.upload_success' => 'PGP-Schluessel gespeichert.', | ||
| 'ui.settings.pgp.openpgp_missing' => 'OpenPGP.js ist auf diesem BBS noch nicht installiert, daher ist die Browser-Schluesselerzeugung nicht verfuegbar.', | ||
| 'ui.settings.pgp.generating' => 'PGP-Schluesselpaar wird erzeugt...', | ||
| 'ui.settings.pgp.generate_success' => 'Verwalteter PGP-Schluessel wurde erzeugt und gespeichert.', | ||
| 'ui.settings.pgp.primary_updated' => 'Primaerer PGP-Schluessel aktualisiert.', | ||
| 'ui.settings.pgp.delete_confirm' => 'Diesen PGP-Schluessel loeschen?', | ||
| 'ui.settings.pgp.delete_success' => 'PGP-Schluessel geloescht.', | ||
| 'ui.settings.pgp.copy_success' => 'Fingerprint kopiert.', | ||
| 'ui.settings.pgp.copy_public_key_button' => 'Oeffentlichen Schluessel kopieren', | ||
| 'ui.settings.pgp.download_public_key_button' => 'Oeffentlichen Schluessel herunterladen', | ||
| 'ui.settings.pgp.copy_public_key_success' => 'Oeffentlicher Schluessel kopiert.', | ||
| 'ui.settings.pgp.key_details_title' => 'PGP-Schluesseldetails', | ||
| 'ui.settings.pgp.algorithm' => 'Algorithmus', | ||
| 'ui.compose.pgp.title' => 'PGP', | ||
| 'ui.compose.pgp.encrypt_netmail' => 'Dieses Netmail verschlüsseln', | ||
| 'ui.compose.pgp.encrypt_netmail_help' => 'Verschlüsselt die Nachricht für den Empfänger mit dessen veröffentlichtem öffentlichen Schlüssel.', | ||
| 'ui.compose.pgp.sign_echomail' => 'Dieses Echomail signieren', | ||
| 'ui.compose.pgp.sign_echomail_help' => 'Signiert die Nachricht mit Deinem gespeicherten verwalteten privaten Schlüssel.', | ||
| 'ui.compose.pgp.encrypt_only_notice' => 'Netmail-Verschluesselung braucht nur den oeffentlichen Schluessel des Empfaengers. Signieren und Entschluesseln erfordern Deinen verwalteten privaten Schluessel.', | ||
| 'ui.compose.pgp.encrypt_recipient_label' => 'Öffentlicher Schlüssel des Empfängers', | ||
| 'ui.compose.pgp.encrypt_recipient_help' => 'Wähle den öffentlichen Schlüssel, mit dem dieses Netmail verschlüsselt wird.', | ||
| 'ui.compose.pgp.encrypt_recipient_placeholder' => 'Öffentlichen Schlüssel zum Verschlüsseln auswählen', | ||
| 'ui.compose.pgp.encrypt_no_key' => 'Kein passender öffentlicher Schlüssel gefunden.', | ||
| 'ui.compose.pgp.encrypt_no_key_help' => 'Gib einen Empfänger mit veröffentlichtem öffentlichen Schlüssel an.', | ||
| 'ui.compose.pgp.encrypting_for' => 'Verschlüsselung für {recipient}', | ||
| 'ui.pgp.notice' => 'PGP-Aktionen verwenden Deinen gespeicherten verwalteten privaten Schlüssel. Wenn Du nur einen öffentlichen Schlüssel hochgeladen hast, sind Signieren und Entschlüsseln nicht verfügbar.', | ||
| 'ui.pgp.decrypt_button' => 'Entschlüsseln', | ||
| 'ui.pgp.decrypt_help' => 'Diese Nachricht ist mit PGP verschlüsselt.', | ||
| 'ui.pgp.passphrase_prompt' => 'Gib Deine PGP-Passphrase ein, um diese Nachricht zu entschlüsseln.', | ||
| 'ui.pgp.decrypted' => 'PGP entschlüsselt', | ||
| 'ui.pgp.verifying' => 'PGP-Signatur wird überprüft...', | ||
| 'ui.pgp.verified' => 'PGP-Signatur bestätigt', | ||
| 'ui.pgp.no_public_key' => 'Öffentlicher PGP-Schlüssel nicht gefunden', | ||
| 'ui.pgp.invalid' => 'Ungültige PGP-Signatur', | ||
| 'ui.keyserver.title' => 'PGP-Keyserver', | ||
| 'ui.keyserver.manage_keys' => 'Meine Schluessel verwalten', | ||
| 'ui.keyserver.search_label' => 'Suche', | ||
| 'ui.keyserver.search_placeholder' => 'Benutzername, Realname, E-Mail oder Fingerprint', | ||
| 'ui.keyserver.search_help' => 'Suchen Sie nach Kontoname, E-Mail-Adresse, vollstaendigem Fingerprint oder einer entfernten qualifizierten Suche wie awehttam@227:1/200 oder foobar@claudes.lovelybits.org.', | ||
| 'ui.keyserver.search_button' => 'Suchen', | ||
| 'ui.keyserver.results_heading' => 'Oeffentliche Schluessel', | ||
| 'ui.keyserver.col_user' => 'Benutzer', | ||
| 'ui.keyserver.col_fingerprint' => 'Fingerprint', | ||
| 'ui.keyserver.col_type' => 'Typ', | ||
| 'ui.keyserver.col_created' => 'Erstellt', | ||
| 'ui.keyserver.col_actions' => 'Aktionen', | ||
| 'ui.keyserver.primary_badge' => 'Primaer', | ||
| 'ui.keyserver.download_button' => 'Herunterladen', | ||
| 'ui.keyserver.no_results' => 'Keine oeffentlichen Schluessel entsprechen Ihrer Suche.', |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# run locally before merge
php scripts/check_i18n_syntax.php --locale=de
git diff -- public_html/sw.jsRepository: awehttam/binkterm-php
Length of output: 214
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== sw.js CACHE_NAME diff ==="
git diff -- public_html/sw.js | sed -n '1,200p'
echo
echo "=== current sw.js CACHE_NAME ==="
rg -n "CACHE_NAME" public_html/sw.js || true
sed -n '1,220p' public_html/sw.js | rg -n "CACHE_NAME|binkcache-" || trueRepository: awehttam/binkterm-php
Length of output: 1215
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "i18n|config/i18n|translator|locale|common\.php|errors\.php|terminalserver\.php|/de/" public_html/sw.js || trueRepository: awehttam/binkterm-php
Length of output: 378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
nl -ba public_html/sw.js | sed -n '1,220p'
nl -ba public_html/sw.js | sed -n '220,320p'Repository: awehttam/binkterm-php
Length of output: 108
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from itertools import islice
path="public_html/sw.js"
with open(path,'r',encoding='utf-8') as f:
lines=list(f)
for start,end in [(120,220),(180,260)]:
print(f"\n=== {path} lines {start}-{end} ===")
for i in range(start-1,end):
if i<0 or i>=len(lines): continue
print(f"{i+1}: {lines[i].rstrip()}")
PYRepository: awehttam/binkterm-php
Length of output: 6064
Update SERVICE WORKER cache version for i18n changes (de/common.php)
php scripts/check_i18n_syntax.php --locale=depasses (includesi18n/de/common.php).public_html/sw.jsCACHE_NAMEwas not incremented in this change; since the service worker caches/api/i18n/catalogcache-first, bumpCACHE_NAMEso clients fetch the updated German catalog.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@config/i18n/de/common.php` around lines 4970 - 5056, The service worker cache
version wasn't bumped after updating i18n/de/common.php, so clients may still
serve the old /api/i18n/catalog; open public_html/sw.js, locate the CACHE_NAME
constant (and any CACHE_VERSION or cache name string) and increment its version
value (e.g. append or increment the numeric suffix) so the service worker treats
caches as changed and fetches the updated German catalog; then rebuild/deploy so
clients get the new sw.js.
| 'ui.settings.tab.pgp' => 'PGP', | ||
| 'ui.settings.pgp.heading' => 'Cles PGP', | ||
| 'ui.settings.pgp.help' => 'Televersez des cles publiques, generez des cles privees gerees par le BBS et choisissez la cle publique principale a publier sur le serveur de cles.', | ||
| 'ui.settings.pgp.upload_heading' => 'Televerser une cle publique', | ||
| 'ui.settings.pgp.key_label' => 'Etiquette de la cle', | ||
| 'ui.settings.pgp.key_label_placeholder' => 'Cle portable, travail, archive', | ||
| 'ui.settings.pgp.public_key' => 'Cle publique ASCII-armored', | ||
| 'ui.settings.pgp.public_key_placeholder' => '-----BEGIN PGP PUBLIC KEY BLOCK-----', | ||
| 'ui.settings.pgp.upload_button' => 'Televerser la cle publique', | ||
| 'ui.settings.pgp.generate_heading' => 'Generer une cle geree par le BBS', | ||
| 'ui.settings.pgp.managed_label_placeholder' => 'Cle geree par BinktermPHP', | ||
| 'ui.settings.pgp.passphrase' => 'Phrase de passe PGP', | ||
| 'ui.settings.pgp.passphrase_placeholder' => 'Saisissez une phrase de passe PGP', | ||
| 'ui.settings.pgp.passphrase_help' => 'Utilisez une phrase de passe differente de votre mot de passe de connexion.', | ||
| 'ui.settings.pgp.passphrase_confirm' => 'Confirmer la phrase de passe', | ||
| 'ui.settings.pgp.passphrase_confirm_placeholder' => 'Saisissez de nouveau votre phrase de passe PGP', | ||
| 'ui.settings.pgp.generate_button' => 'Generer la cle geree', | ||
| 'ui.settings.pgp.generate_help' => 'Cela genere la paire de cles dans le navigateur et seul le blob chiffre de la cle privee est stocke sur le serveur.', | ||
| 'ui.settings.pgp.managed_disabled_notice' => 'Ce BBS autorise le televersement de cles publiques PGP, mais l\'hebergement de cles privees gerees par le BBS est desactive par le sysop.', | ||
| 'ui.settings.pgp.saved_keys_heading' => 'Cles enregistrees', | ||
| 'ui.settings.pgp.keyserver_button' => 'Parcourir le serveur de cles', | ||
| 'ui.settings.pgp.no_keys' => 'Aucune cle PGP enregistree pour le moment.', | ||
| 'ui.settings.pgp.col_primary' => 'Statut', | ||
| 'ui.settings.pgp.col_label' => 'Etiquette', | ||
| 'ui.settings.pgp.col_fingerprint' => 'Empreinte', | ||
| 'ui.settings.pgp.col_source' => 'Source', | ||
| 'ui.settings.pgp.col_created' => 'Creation', | ||
| 'ui.settings.pgp.col_actions' => 'Actions', | ||
| 'ui.settings.pgp.source_managed' => 'Geree par le BBS', | ||
| 'ui.settings.pgp.source_uploaded' => 'Televersee', | ||
| 'ui.settings.pgp.private_key_available' => 'Cle privee chiffree stockee', | ||
| 'ui.settings.pgp.primary_selector' => 'Cle publique preferee', | ||
| 'ui.settings.pgp.primary_help' => 'Choisissez ici une cle, ou utilisez le bouton du tableau ci-dessous pour la definir comme principale, afin de changer la cle publique affichee comme preferee sur le serveur de cles.', | ||
| 'ui.settings.pgp.primary_selected' => 'Principale', | ||
| 'ui.settings.pgp.secondary_key' => 'Secondaire', | ||
| 'ui.settings.pgp.make_primary_button' => 'Definir principale', | ||
| 'ui.settings.pgp.current_primary_button' => 'Principale actuelle', | ||
| 'ui.settings.pgp.uploading' => 'Televersement de la cle publique...', | ||
| 'ui.settings.pgp.upload_success' => 'Cle PGP enregistree.', | ||
| 'ui.settings.pgp.openpgp_missing' => 'OpenPGP.js n\'est pas encore installe sur ce BBS, donc la generation de cles dans le navigateur n\'est pas disponible.', | ||
| 'ui.settings.pgp.generating' => 'Generation de la paire de cles PGP...', | ||
| 'ui.settings.pgp.generate_success' => 'Cle PGP geree generee et enregistree.', | ||
| 'ui.settings.pgp.primary_updated' => 'Cle PGP principale mise a jour.', | ||
| 'ui.settings.pgp.delete_confirm' => 'Supprimer cette cle PGP ?', | ||
| 'ui.settings.pgp.delete_success' => 'Cle PGP supprimee.', | ||
| 'ui.settings.pgp.copy_success' => 'Empreinte copiee.', | ||
| 'ui.settings.pgp.copy_public_key_button' => 'Copier la cle publique', | ||
| 'ui.settings.pgp.download_public_key_button' => 'Telecharger la cle publique', | ||
| 'ui.settings.pgp.copy_public_key_success' => 'Cle publique copiee.', | ||
| 'ui.settings.pgp.key_details_title' => 'Détails de la clé PGP', | ||
| 'ui.settings.pgp.algorithm' => 'Algorithme', | ||
| 'ui.compose.pgp.title' => 'PGP', | ||
| 'ui.compose.pgp.encrypt_netmail' => 'Chiffrer ce netmail', | ||
| 'ui.compose.pgp.encrypt_netmail_help' => 'Chiffre le message pour le destinataire a l’aide de sa cle publique publiee.', | ||
| 'ui.compose.pgp.sign_echomail' => 'Signer cet echomail', | ||
| 'ui.compose.pgp.sign_echomail_help' => 'Signe le message avec votre cle privee geree stockee.', | ||
| 'ui.compose.pgp.encrypt_only_notice' => 'Le chiffrement netmail a seulement besoin de la cle publique du destinataire. La signature et le dechiffrement exigent votre cle privee geree.', | ||
| 'ui.compose.pgp.encrypt_recipient_label' => 'Cle publique du destinataire', | ||
| 'ui.compose.pgp.encrypt_recipient_help' => 'Choisissez la cle publique qui sera utilisee pour chiffrer ce netmail.', | ||
| 'ui.compose.pgp.encrypt_recipient_placeholder' => 'Selectionnez la cle publique pour le chiffrement', | ||
| 'ui.compose.pgp.encrypt_no_key' => 'Aucune cle publique correspondante trouvee.', | ||
| 'ui.compose.pgp.encrypt_no_key_help' => 'Saisissez un destinataire disposant d’une cle publique publiee.', | ||
| 'ui.compose.pgp.encrypting_for' => 'Chiffrement pour {recipient}', | ||
| 'ui.pgp.notice' => 'Les actions PGP utilisent votre cle privee geree stockee. Si vous avez seulement televerse une cle publique, la signature et le dechiffrement ne sont pas disponibles.', | ||
| 'ui.pgp.decrypt_button' => 'Dechiffrer', | ||
| 'ui.pgp.decrypt_help' => 'Ce message est chiffre avec PGP.', | ||
| 'ui.pgp.passphrase_prompt' => 'Saisissez votre phrase de passe PGP pour dechiffrer ce message.', | ||
| 'ui.pgp.decrypted' => 'PGP dechiffre', | ||
| 'ui.pgp.verifying' => 'Verification de la signature PGP...', | ||
| 'ui.pgp.verified' => 'Signature PGP verifiee', | ||
| 'ui.pgp.no_public_key' => 'Cle publique PGP introuvable', | ||
| 'ui.pgp.invalid' => 'Signature PGP invalide', | ||
| 'ui.keyserver.title' => 'Serveur de cles PGP', | ||
| 'ui.keyserver.manage_keys' => 'Gerer mes cles', | ||
| 'ui.keyserver.search_label' => 'Recherche', | ||
| 'ui.keyserver.search_placeholder' => 'Nom d\'utilisateur, nom reel, courriel ou empreinte', | ||
| 'ui.keyserver.search_help' => 'Recherchez par nom de compte, adresse courriel, empreinte complete ou recherche distante qualifiee comme awehttam@227:1/200 ou foobar@claudes.lovelybits.org.', | ||
| 'ui.keyserver.search_button' => 'Rechercher', | ||
| 'ui.keyserver.results_heading' => 'Cles publiques', | ||
| 'ui.keyserver.col_user' => 'Utilisateur', | ||
| 'ui.keyserver.col_fingerprint' => 'Empreinte', | ||
| 'ui.keyserver.col_type' => 'Type', | ||
| 'ui.keyserver.col_created' => 'Creation', | ||
| 'ui.keyserver.col_actions' => 'Actions', | ||
| 'ui.keyserver.primary_badge' => 'Principale', | ||
| 'ui.keyserver.download_button' => 'Telecharger', | ||
| 'ui.keyserver.no_results' => 'Aucune cle publique ne correspond a votre recherche.', |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Required syntax/encoding checks for edited FR catalog
php scripts/check_i18n_syntax.php --locale=fr
# 2) Repo-specific i18n checks from project learnings
php scripts/check_i18n_hardcoded_strings.php
php scripts/check_i18n_error_keys.php
# 3) Verify new FR keys exist in other locales' common catalogs
python - <<'PY'
import re, pathlib, sys
locales = ["en","de","es","it","fr"]
files = {loc: pathlib.Path(f"config/i18n/{loc}/common.php") for loc in locales}
for loc,p in files.items():
if not p.exists():
print(f"[missing_file] {p}")
sys.exit(1)
pat = re.compile(r"^\s*'([^']+)'\s*=>", re.M)
keys = {loc: set(pat.findall(files[loc].read_text(encoding="utf-8", errors="strict"))) for loc in locales}
fr_new = {k for k in keys["fr"] if (
k.startswith("ui.settings.pgp.")
or k.startswith("ui.compose.pgp.")
or k.startswith("ui.keyserver.")
or k.startswith("ui.pgp.")
or k in {
"ui.settings.tab.pgp",
"ui.address_book.pgp_public_key",
"ui.address_book.pgp_public_key_placeholder",
"ui.address_book.pgp_public_key_help",
"ui.address_book.pgp_key_linked",
"ui.admin.bbs_settings.features.enable_pgp",
"ui.admin.bbs_settings.features.pgp_help",
"ui.admin.bbs_settings.features.enable_pgp_managed_keys",
"ui.admin.bbs_settings.features.pgp_managed_keys_help",
}
)}
missing = {}
for loc in ["en","de","es","it"]:
diff = sorted(fr_new - keys[loc])
if diff:
missing[loc] = diff
if not fr_new:
print("[warn] No targeted FR PGP keys found by detector.")
if missing:
for loc, diff in missing.items():
print(f"\n[missing_in_{loc}]")
for k in diff:
print(k)
sys.exit(2)
print("[ok] Targeted FR PGP keys exist in en/de/es/it common catalogs.")
PY
# 4) Verify service worker cache name was updated in this PR branch
rg -n "CACHE_NAME" public_html/sw.jsRepository: awehttam/binkterm-php
Length of output: 1046
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show whether CACHE_NAME changed in this PR for sw.js
git diff --unified=0 -- public_html/sw.js | rg -n "CACHE_NAME|binkcache" || true
# Also show the exact current CACHE_NAME line for context
rg -n "const CACHE_NAME" public_html/sw.jsRepository: awehttam/binkterm-php
Length of output: 104
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the exact change set for sw.js (focus on CACHE_NAME)
git diff --unified=5 -- public_html/sw.js | rg -n "CACHE_NAME|binkcache|Version|purge|UPDATE_AVAILABLE" || true
# Also show minimal diff stats for sw.js
git diff --stat -- public_html/sw.js || trueRepository: awehttam/binkterm-php
Length of output: 47
🏁 Script executed:
#!/bin/bash
set -euo pipefail
git rev-parse --is-inside-work-tree
git status --porcelain=v1
git diff --name-only
git diff --stat
git diff -- public_html/sw.js | head -n 80 || trueRepository: awehttam/binkterm-php
Length of output: 410
🏁 Script executed:
#!/bin/bash
set -euo pipefail
if git diff --quiet -- public_html/sw.js; then
echo "[sw.js] no diff vs HEAD"
else
echo "[sw.js] has diff vs HEAD"
git diff -- public_html/sw.js | head -n 120
fiRepository: awehttam/binkterm-php
Length of output: 89
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Identify available refs/branches to find PR base
git branch -a || true
git show-ref --heads --tags | head -n 200 || true
git remote -v || true
# Try common base refs; if they don't exist, commands will fail safely
for base in origin/main origin/master origin/develop origin/trunk; do
if git show-ref --verify --quiet "refs/remotes/${base}"; then
echo "=== diff vs ${base} ==="
git diff --stat "${base}...HEAD" -- public_html/sw.js || true
git diff "${base}...HEAD" -- public_html/sw.js | rg -n "CACHE_NAME|binkcache|v[0-9]+" || true
fi
done
# Fallback: diff against merge-base with origin/main if present
if git show-ref --verify --quiet "refs/remotes/origin/main"; then
mb=$(git merge-base origin/main HEAD)
echo "=== merge-base: ${mb} diff ==="
git diff --stat "${mb}..HEAD" -- public_html/sw.js || true
git diff "${mb}..HEAD" -- public_html/sw.js | rg -n "CACHE_NAME|binkcache|v[0-9]+" || true
fiRepository: awehttam/binkterm-php
Length of output: 479
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "HEAD CACHE_NAME:"
git show HEAD:public_html/sw.js | rg -n "const CACHE_NAME"
echo "origin/main CACHE_NAME:"
git show origin/main:public_html/sw.js | rg -n "const CACHE_NAME"
echo "Compare sw.js (only CACHE_NAME line diff, if any):"
git diff --unified=3 origin/main -- public_html/sw.js | rg -n "CACHE_NAME|binkcache|v[0-9]+" || true
echo "sw.js blob ids:"
echo -n "HEAD: "; git rev-parse HEAD:public_html/sw.js
echo -n "origin/main: "; git rev-parse origin/main:public_html/sw.jsRepository: awehttam/binkterm-php
Length of output: 432
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path("config/i18n/fr/common.php")
data = p.read_bytes()
# Find any UTF-8 sequences for curly/smart quotes U+2018/U+2019
# U+2018 = ’? actually U+2018 is LEFT SINGLE QUOTATION MARK (’ in many fonts) -> bytes: E2 80 98
# U+2019 = RIGHT SINGLE QUOTATION MARK -> bytes: E2 80 99
bad = []
for cp, seq in [("U+2018", b"\xE2\x80\x98"), ("U+2019", b"\xE2\x80\x99")]:
idx = data.find(seq)
if idx != -1:
bad.append((cp, idx))
print("[curly_quotes_bytes_findings]")
if not bad:
print("none")
else:
for cp, idx in bad:
print(cp, "at byte offset", idx)
# Also print the literal offending surrounding context (if any)
s = data.decode("utf-8", errors="replace")
for cp in ["\u2018", "\u2019"]:
if cp in s:
i = s.index(cp)
print("\nFirst occurrence context for", cp, ":")
print(s[max(0,i-80):min(len(s),i+80)])
PYRepository: awehttam/binkterm-php
Length of output: 317
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path("config/i18n/fr/common.php")
data = path.read_text(encoding="utf-8", errors="strict")
needle = "\u2019" # U+2019 RIGHT SINGLE QUOTATION MARK
indices = []
start = 0
while True:
i = data.find(needle, start)
if i == -1:
break
indices.append(i)
start = i + 1
print(f"[count] {len(indices)} occurrences of U+2019 in {path}")
# Map offsets to line numbers
line_starts = []
pos = 0
for line in data.splitlines(True):
line_starts.append(pos)
pos += len(line)
def offset_to_line(off):
# find rightmost line_start <= off
lo, hi = 0, len(line_starts)-1
while lo <= hi:
mid = (lo+hi)//2
if line_starts[mid] <= off:
lo = mid + 1
else:
hi = mid - 1
line_idx = hi
return line_idx + 1 # 1-based
for off in indices[:20]:
ln = offset_to_line(off)
# print the whole line trimmed + key prefix if possible
line = data.splitlines()[ln-1]
# try extract key inside single quotes at start of assignment
key = None
if "=>" in line:
left = line.split("=>",1)[0].strip()
if left.startswith("'") and left.endswith("'"):
key = left[1:-1]
print(f"\n[U+2019] line {ln} key={key!r}")
print(line[:300])
PY
# Additionally, show whether the exact string from the review snippet contains U+2019
rg -n "l’aide|l\u2019aide" config/i18n/fr/common.php || trueRepository: awehttam/binkterm-php
Length of output: 2390
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="config/i18n/fr/common.php"
# Show git diff hunks for the PGP keys that include U+2019 in the current file
git diff -U3 origin/main -- "$FILE" | rg -n "ui\\.compose\\.pgp\\.encrypt_netmail_help|ui\\.compose\\.pgp\\.encrypt_no_key_help|ui\\.echomail\\.ignore\\.|ui\\.settings\\.echomail_ignore_rules\\.|ui\\.admin\\.ads\\.form_hidden_warning|ui\\.admin\\.filearea_rules\\.no_areas" || true
python3 - <<'PY'
import re
from pathlib import Path
path = Path("config/i18n/fr/common.php")
def extract(content, key):
# key is like ui.compose.pgp.encrypt_netmail_help
# match: 'key' => 'value',
m = re.search(rf"^\s*'{re.escape(key)}'\s*=>\s*'((?:\\'|[^'])*?)'\s*,\s*$", content, flags=re.M)
return m.group(1) if m else None
keys = [
"ui.compose.pgp.encrypt_netmail_help",
"ui.compose.pgp.encrypt_no_key_help",
]
head = path.read_text(encoding="utf-8", errors="strict")
base = (Path(".git")/"")
import subprocess
origin = subprocess.check_output(["git","show","origin/main:config/i18n/fr/common.php"], text=True)
for k in keys:
h = extract(head, k)
o = extract(origin, k)
print(f"\n== {k} ==")
print("origin/main value:", o)
print("HEAD value: ", h)
PYRepository: awehttam/binkterm-php
Length of output: 621
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="config/i18n/fr/common.php"
# Show added lines (in this PR) that contain U+2019
git diff -U0 origin/main -- "$FILE" | rg -n "\+.*\u2019|\\xE2\\x80\\x99|[’]" || true
# Specifically list which PGP-related keys in the added diff contain that character
git diff -U0 origin/main -- "$FILE" \
| rg -n "^\+.*'ui\.(settings|compose|pgp|keyserver|echomail|address_book|admin)\.pgp\..*'|^\+.*'ui\.(compose|pgp)\.pgp\..*'|^\+.*'ui\.compose\.pgp\..*'|^\+.*'ui\.settings\.pgp\..*'|^\+.*'ui\.keyserver\..*'|^\+.*'ui\.pgp\..*'|^\+.*'ui\.address_book\.pgp_.*'|^\+.*'ui\.admin\.bbs_settings\.features\.(enable_pgp|pgp_help|enable_pgp_managed_keys|pgp_managed_keys_help)'" -n || trueRepository: awehttam/binkterm-php
Length of output: 8434
Fix prohibited curly apostrophes in the new FR PGP strings; other i18n + parity + cache work looks good.
- i18n checks for
frpass (check_i18n_syntax.php,check_i18n_hardcoded_strings.php,check_i18n_error_keys.php). - The targeted new PGP keys are present in
config/i18n/{en,de,es,it}/common.php(locale parity holds). public_html/sw.jscache bump is present (binkcache-v908→binkcache-v927).config/i18n/fr/common.phpcontains U+2019 curly apostrophes in two new strings—replace with escaped ASCII apostrophes:ui.compose.pgp.encrypt_netmail_help:l’aide→l\'aideui.compose.pgp.encrypt_no_key_help:d’une→d\'une
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@config/i18n/fr/common.php` around lines 4897 - 4983, Two French PGP strings
contain U+2019 curly apostrophes; open the i18n entries for
ui.compose.pgp.encrypt_netmail_help and ui.compose.pgp.encrypt_no_key_help and
replace the curly apostrophe characters with an escaped ASCII apostrophe (\' )
so the strings read "l\'aide" and "d\'une" respectively, then save and re-run
i18n checks.
| 'ui.compose.pgp.encrypt_netmail_help' => 'Chiffre le message pour le destinataire a l’aide de sa cle publique publiee.', | ||
| 'ui.compose.pgp.sign_echomail' => 'Signer cet echomail', | ||
| 'ui.compose.pgp.sign_echomail_help' => 'Signe le message avec votre cle privee geree stockee.', | ||
| 'ui.compose.pgp.encrypt_only_notice' => 'Le chiffrement netmail a seulement besoin de la cle publique du destinataire. La signature et le dechiffrement exigent votre cle privee geree.', | ||
| 'ui.compose.pgp.encrypt_recipient_label' => 'Cle publique du destinataire', | ||
| 'ui.compose.pgp.encrypt_recipient_help' => 'Choisissez la cle publique qui sera utilisee pour chiffrer ce netmail.', | ||
| 'ui.compose.pgp.encrypt_recipient_placeholder' => 'Selectionnez la cle publique pour le chiffrement', | ||
| 'ui.compose.pgp.encrypt_no_key' => 'Aucune cle publique correspondante trouvee.', | ||
| 'ui.compose.pgp.encrypt_no_key_help' => 'Saisissez un destinataire disposant d’une cle publique publiee.', |
There was a problem hiding this comment.
Replace typographic apostrophes with escaped ASCII apostrophes in catalog values.
These strings include curly apostrophes (’), which are disallowed in i18n PHP catalogs.
Suggested patch
- 'ui.compose.pgp.encrypt_netmail_help' => 'Chiffre le message pour le destinataire a l’aide de sa cle publique publiee.',
+ 'ui.compose.pgp.encrypt_netmail_help' => 'Chiffre le message pour le destinataire a l\'aide de sa cle publique publiee.',
...
- 'ui.compose.pgp.encrypt_no_key_help' => 'Saisissez un destinataire disposant d’une cle publique publiee.',
+ 'ui.compose.pgp.encrypt_no_key_help' => 'Saisissez un destinataire disposant d\'une cle publique publiee.',As per coding guidelines, “Any apostrophe inside a PHP string value in i18n catalogs must be written as \'” and “U+2018/U+2019 (curly/smart quotes) must never appear anywhere in a catalog file.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 'ui.compose.pgp.encrypt_netmail_help' => 'Chiffre le message pour le destinataire a l’aide de sa cle publique publiee.', | |
| 'ui.compose.pgp.sign_echomail' => 'Signer cet echomail', | |
| 'ui.compose.pgp.sign_echomail_help' => 'Signe le message avec votre cle privee geree stockee.', | |
| 'ui.compose.pgp.encrypt_only_notice' => 'Le chiffrement netmail a seulement besoin de la cle publique du destinataire. La signature et le dechiffrement exigent votre cle privee geree.', | |
| 'ui.compose.pgp.encrypt_recipient_label' => 'Cle publique du destinataire', | |
| 'ui.compose.pgp.encrypt_recipient_help' => 'Choisissez la cle publique qui sera utilisee pour chiffrer ce netmail.', | |
| 'ui.compose.pgp.encrypt_recipient_placeholder' => 'Selectionnez la cle publique pour le chiffrement', | |
| 'ui.compose.pgp.encrypt_no_key' => 'Aucune cle publique correspondante trouvee.', | |
| 'ui.compose.pgp.encrypt_no_key_help' => 'Saisissez un destinataire disposant d’une cle publique publiee.', | |
| 'ui.compose.pgp.encrypt_netmail_help' => 'Chiffre le message pour le destinataire a l\'aide de sa cle publique publiee.', | |
| 'ui.compose.pgp.sign_echomail' => 'Signer cet echomail', | |
| 'ui.compose.pgp.sign_echomail_help' => 'Signe le message avec votre cle privee geree stockee.', | |
| 'ui.compose.pgp.encrypt_only_notice' => 'Le chiffrement netmail a seulement besoin de la cle publique du destinataire. La signature et le dechiffrement exigent votre cle privee geree.', | |
| 'ui.compose.pgp.encrypt_recipient_label' => 'Cle publique du destinataire', | |
| 'ui.compose.pgp.encrypt_recipient_help' => 'Choisissez la cle publique qui sera utilisee pour chiffrer ce netmail.', | |
| 'ui.compose.pgp.encrypt_recipient_placeholder' => 'Selectionnez la cle publique pour le chiffrement', | |
| 'ui.compose.pgp.encrypt_no_key' => 'Aucune cle publique correspondante trouvee.', | |
| 'ui.compose.pgp.encrypt_no_key_help' => 'Saisissez un destinataire disposant d\'une cle publique publiee.', |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@config/i18n/fr/common.php` around lines 4950 - 4958, The reported strings use
curly apostrophes (U+2019) which are forbidden; replace each curly apostrophe in
the catalog values with an escaped ASCII apostrophe (\' ) so the PHP string
literals remain valid — update the values for keys such as
ui.compose.pgp.encrypt_netmail_help, ui.compose.pgp.sign_echomail_help,
ui.compose.pgp.encrypt_only_notice, ui.compose.pgp.encrypt_recipient_help,
ui.compose.pgp.encrypt_recipient_placeholder, and
ui.compose.pgp.encrypt_no_key_help to use \' instead of ’ throughout the file.
| 'errors.pgp.decrypt_failed' => 'Impossible de dechiffrer le message PGP.', | ||
| 'errors.pgp.recipient_required' => 'Impossible de resoudre une cle publique pour le destinataire.', | ||
| 'errors.pgp.passphrase_required' => 'Saisissez votre phrase de passe PGP.', | ||
| 'errors.pgp.private_key_required' => 'Une cle privee geree est requise pour signer.', |
There was a problem hiding this comment.
Fix missing French accents in PGP error messages.
Several words are missing required French accents. As per coding guidelines, "use literal characters (e.g., 'é', 'à') only" when editing French catalogs.
Required corrections:
- Line 65:
dechiffrer→déchiffrer - Line 66:
resoudre→résoudre,cle→clé - Line 68:
cle→clé,privee→privée,geree→gérée
🔧 Proposed fix
- 'errors.pgp.decrypt_failed' => 'Impossible de dechiffrer le message PGP.',
- 'errors.pgp.recipient_required' => 'Impossible de resoudre une cle publique pour le destinataire.',
+ 'errors.pgp.decrypt_failed' => 'Impossible de déchiffrer le message PGP.',
+ 'errors.pgp.recipient_required' => 'Impossible de résoudre une clé publique pour le destinataire.',
'errors.pgp.passphrase_required' => 'Saisissez votre phrase de passe PGP.',
- 'errors.pgp.private_key_required' => 'Une cle privee geree est requise pour signer.',
+ 'errors.pgp.private_key_required' => 'Une clé privée gérée est requise pour signer.',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 'errors.pgp.decrypt_failed' => 'Impossible de dechiffrer le message PGP.', | |
| 'errors.pgp.recipient_required' => 'Impossible de resoudre une cle publique pour le destinataire.', | |
| 'errors.pgp.passphrase_required' => 'Saisissez votre phrase de passe PGP.', | |
| 'errors.pgp.private_key_required' => 'Une cle privee geree est requise pour signer.', | |
| 'errors.pgp.decrypt_failed' => 'Impossible de déchiffrer le message PGP.', | |
| 'errors.pgp.recipient_required' => 'Impossible de résoudre une clé publique pour le destinataire.', | |
| 'errors.pgp.passphrase_required' => 'Saisissez votre phrase de passe PGP.', | |
| 'errors.pgp.private_key_required' => 'Une clé privée gérée est requise pour signer.', |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@config/i18n/fr/errors.php` around lines 65 - 68, The French PGP error
messages use unaccented characters; update the translation strings for the keys
'errors.pgp.decrypt_failed', 'errors.pgp.recipient_required', and
'errors.pgp.private_key_required' to use proper French accents: change
"dechiffrer" → "déchiffrer" in errors.pgp.decrypt_failed; in
errors.pgp.recipient_required change "resoudre" → "résoudre" and "cle" → "clé";
in errors.pgp.private_key_required change "cle" → "clé", "privee" → "privée" and
"geree" → "gérée". Ensure you edit the literal string values (not escaped
entities) so the file contains the accented characters directly.
| 'errors.pgp.load_failed' => 'Impossible de charger les cles PGP.', | ||
| 'errors.pgp.public_key_required' => 'Une cle publique est requise.', | ||
| 'errors.pgp.invalid_key' => 'Cle publique PGP invalide.', | ||
| 'errors.pgp.save_failed' => 'Impossible d\'enregistrer la cle PGP.', | ||
| 'errors.pgp.invalid_keypair' => 'Paire de cles PGP invalide.', | ||
| 'errors.pgp.key_not_found' => 'Cle PGP introuvable.', | ||
| 'errors.pgp.private_key_not_found' => 'Cle privee introuvable.', | ||
| 'errors.pgp.delete_failed' => 'Impossible de supprimer la cle PGP.', | ||
| 'errors.pgp.disabled' => 'PGP est desactive sur ce systeme.', | ||
| 'errors.pgp.managed_disabled' => 'La generation de cles PGP gerees est desactivee sur ce systeme.', | ||
| 'errors.pgp.passphrase_too_short' => 'Utilisez une phrase de passe PGP plus longue.', | ||
| 'errors.pgp.passphrase_mismatch' => 'La confirmation de la phrase de passe ne correspond pas.', | ||
| 'errors.pgp.generation_failed' => 'Impossible de generer la cle PGP geree.', |
There was a problem hiding this comment.
Fix extensive missing French accents in PGP management errors.
Nearly every line in this segment is missing required French accents. As per coding guidelines, accented catalogs must use literal characters like 'é', 'à'.
Required corrections:
- Lines 686-693:
cle/cles→clé/clés(8 occurrences) - Line 692:
privee→privée - Lines 694-695:
desactive/desactivee→désactivé/désactivée - Lines 694-695:
systeme→système(2 occurrences) - Lines 695, 698:
generation/generer/geree/gerees→génération/générer/gérée/gérées
Note: Line 689 correctly uses d\'enregistrer with escaped apostrophe, but the word "cle" still needs the accent.
🔧 Proposed fix
- 'errors.pgp.load_failed' => 'Impossible de charger les cles PGP.',
- 'errors.pgp.public_key_required' => 'Une cle publique est requise.',
- 'errors.pgp.invalid_key' => 'Cle publique PGP invalide.',
- 'errors.pgp.save_failed' => 'Impossible d\'enregistrer la cle PGP.',
- 'errors.pgp.invalid_keypair' => 'Paire de cles PGP invalide.',
- 'errors.pgp.key_not_found' => 'Cle PGP introuvable.',
- 'errors.pgp.private_key_not_found' => 'Cle privee introuvable.',
- 'errors.pgp.delete_failed' => 'Impossible de supprimer la cle PGP.',
- 'errors.pgp.disabled' => 'PGP est desactive sur ce systeme.',
- 'errors.pgp.managed_disabled' => 'La generation de cles PGP gerees est desactivee sur ce systeme.',
+ 'errors.pgp.load_failed' => 'Impossible de charger les clés PGP.',
+ 'errors.pgp.public_key_required' => 'Une clé publique est requise.',
+ 'errors.pgp.invalid_key' => 'Clé publique PGP invalide.',
+ 'errors.pgp.save_failed' => 'Impossible d\'enregistrer la clé PGP.',
+ 'errors.pgp.invalid_keypair' => 'Paire de clés PGP invalide.',
+ 'errors.pgp.key_not_found' => 'Clé PGP introuvable.',
+ 'errors.pgp.private_key_not_found' => 'Clé privée introuvable.',
+ 'errors.pgp.delete_failed' => 'Impossible de supprimer la clé PGP.',
+ 'errors.pgp.disabled' => 'PGP est désactivé sur ce système.',
+ 'errors.pgp.managed_disabled' => 'La génération de clés PGP gérées est désactivée sur ce système.',
'errors.pgp.passphrase_too_short' => 'Utilisez une phrase de passe PGP plus longue.',
'errors.pgp.passphrase_mismatch' => 'La confirmation de la phrase de passe ne correspond pas.',
- 'errors.pgp.generation_failed' => 'Impossible de generer la cle PGP geree.',
+ 'errors.pgp.generation_failed' => 'Impossible de générer la clé PGP gérée.',🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@config/i18n/fr/errors.php` around lines 686 - 698, Update the French error
strings for PGP keys to use proper accented characters: replace "cle"/"cles"
with "clé"/"clés" in keys like 'errors.pgp.load_failed',
'errors.pgp.public_key_required', 'errors.pgp.invalid_key',
'errors.pgp.save_failed', 'errors.pgp.invalid_keypair',
'errors.pgp.key_not_found', 'errors.pgp.private_key_not_found', and
'errors.pgp.delete_failed'; change "privee" to "privée" in
'errors.pgp.private_key_not_found'; change "desactive"/"desactivee" to
"désactivé"/"désactivée" and "systeme" to "système" in 'errors.pgp.disabled' and
'errors.pgp.managed_disabled'; and change
"generation"/"generer"/"geree"/"gerees" to
"génération"/"générer"/"gérée"/"gérées" in 'errors.pgp.managed_disabled' and
'errors.pgp.generation_failed', while preserving existing escaping for
apostrophes (e.g., keep d\'enregistrer).
| } | ||
|
|
||
| $finalMessageText = $this->applyUserSignatureAndTagline($messageText, $fromUserId, $tagline); | ||
| $finalMessageText = $pgpMode ? $messageText : $this->applyUserSignatureAndTagline($messageText, $fromUserId, $tagline); |
There was a problem hiding this comment.
Use strict boolean gating for $pgpMode.
Line 1402 and Line 1816 use loose truthiness ($pgpMode ? ...). If callers pass string values (for example 'false'), PHP treats them as truthy and incorrectly skips signature/tagline application.
Suggested fix
- $finalMessageText = $pgpMode ? $messageText : $this->applyUserSignatureAndTagline($messageText, $fromUserId, $tagline);
+ $isPgpMode = filter_var($pgpMode, FILTER_VALIDATE_BOOLEAN);
+ $finalMessageText = $isPgpMode ? $messageText : $this->applyUserSignatureAndTagline($messageText, $fromUserId, $tagline);- $finalMessageText = $pgpMode ? $messageText : $this->applyUserSignatureAndTagline($messageText, $fromUserId, $tagline);
+ $isPgpMode = filter_var($pgpMode, FILTER_VALIDATE_BOOLEAN);
+ $finalMessageText = $isPgpMode ? $messageText : $this->applyUserSignatureAndTagline($messageText, $fromUserId, $tagline);Also applies to: 1816-1816
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/MessageHandler.php` at line 1402, The ternary uses loose truthiness for
$pgpMode which treats values like the string 'false' as true; change the
condition to a strict boolean check (e.g. use $pgpMode === true) when deciding
whether to skip applyUserSignatureAndTagline so only a real boolean true
disables signature/tagline; update both occurrences (the one assigning
$finalMessageText and the other at the second occurrence around line 1816) to
use === true (or an explicit is_bool check plus === true) referencing the
$pgpMode variable and the applyUserSignatureAndTagline(...) call.
| private function readOldPacketLength(string $binary, int $offset, int $lengthType): array | ||
| { | ||
| return match ($lengthType) { | ||
| 0 => [ord($binary[$offset] ?? "\0"), $offset + 1], | ||
| 1 => [(int)unpack('n', substr($binary, $offset, 2))[1], $offset + 2], | ||
| 2 => [(int)unpack('N', substr($binary, $offset, 4))[1], $offset + 4], | ||
| default => throw new InvalidArgumentException('Indeterminate OpenPGP packet lengths are not supported.'), | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the target file
FILE="src/Pgp/ArmoredPublicKeyParser.php"
echo "== File check =="
ls -l "$FILE"
# Show the exact function and nearby code (line range around 180-260)
echo "== readOldPacketLength() and nearby context =="
nl -ba "$FILE" | sed -n '160,260p'
# Find call sites of readOldPacketLength()
echo "== Call sites =="
rg -n "readOldPacketLength\s*\(" "$FILE"
# Show the function that calls parsePackets/length readers (search parsePackets)
echo "== parsePackets() =="
rg -n "function\s+parsePackets|parsePackets\s*\(" "$FILE"
# Print around parsePackets if found
LINE=$(rg -n "function\s+parsePackets" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${LINE:-}" ]; then
START=$((LINE-60))
END=$((LINE+120))
nl -ba "$FILE" | sed -n "${START},${END}p"
fi
# Search for any existing buffer-length guards or exception handling
echo "== Buffer/length guards near decoding =="
rg -n "(strlen\(|substr\(|unpack\(|offset|lengthType|truncated|InvalidArgumentException|throw)" "$FILE"Repository: awehttam/binkterm-php
Length of output: 275
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/Pgp/ArmoredPublicKeyParser.php"
echo "== File check =="
ls -l "$FILE"
echo "== readOldPacketLength() context (lines 180-260) =="
python3 - <<'PY'
import itertools
path = "src/Pgp/ArmoredPublicKeyParser.php"
start, end = 180, 260
with open(path, 'r', encoding='utf-8') as f:
for i, line in enumerate(f, start=1):
if start <= i <= end:
print(f"{i:5d}\t{line.rstrip()}")
if i > end:
break
PY
echo "== Call sites of readOldPacketLength() =="
rg -n "readOldPacketLength\s*\(" "$FILE" || true
echo "== parsePackets() context (search and dump nearby) =="
LINE=$(rg -n "function\s+parsePackets|parsePackets\s*\(" "$FILE" | head -n1 | cut -d: -f1 || true)
echo "First parsePackets-related line: ${LINE:-<none>}"
if [ -n "${LINE:-}" ]; then
python3 - <<'PY'
path="src/Pgp/ArmoredPublicKeyParser.php"
target_line=int("""'"${LINE:-0}"'""")
start=max(1,target_line-80)
end=target_line+180
with open(path,'r',encoding='utf-8') as f:
for i,line in enumerate(f, start=1):
if start<=i<=end:
print(f"{i:5d}\t{line.rstrip()}")
if i>end:
break
PY
fi
echo "== Decoding/length guards around substr/unpack calls =="
rg -n "(unpack\s*\(|substr\s*\(|strlen\s*\(|InvalidArgumentException|throw|offset\s*[+\-]?=|lengthType)" "$FILE"Repository: awehttam/binkterm-php
Length of output: 3308
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/Pgp/ArmoredPublicKeyParser.php"
echo "== Call site context (lines 110-190) =="
python3 - <<'PY'
path="src/Pgp/ArmoredPublicKeyParser.php"
start,end=110,190
with open(path,'r',encoding='utf-8') as f:
for i,line in enumerate(f, start=1):
if start<=i<=end:
print(f"{i:5d}\t{line.rstrip()}")
if i>end:
break
PY
echo "== File header/top (lines 1-120) =="
python3 - <<'PY'
path="src/Pgp/ArmoredPublicKeyParser.php"
start,end=1,120
with open(path,'r',encoding='utf-8') as f:
for i,line in enumerate(f, start=1):
if start<=i<=end:
print(f"{i:5d}\t{line.rstrip()}")
if i>end:
break
PY
echo "== Searching for existing truncated-length guards =="
rg -n "Truncated OpenPGP packet length|Partial OpenPGP packet lengths|unpack\\('n'|unpack\\('N'|substr\\(\\$binary, \\$offset, (2|4)" "$FILE" || true
echo "== Searching for pre-check on offset/buffer length before readOldPacketLength =="
rg -n "readOldPacketLength\\(|\\$offset\\s*[+\\-]=|strlen\\(\\$binary\\)" "$FILE"Repository: awehttam/binkterm-php
Length of output: 7775
Reject truncated old-format packet lengths before calling unpack().
parsePackets() only checks ($offset + $packetLength) > $length after readOldPacketLength() returns; for old-format lengthType 1/2, readOldPacketLength() calls unpack() on substr($binary, $offset, 2|4) without ensuring those bytes exist, so malformed/truncated input can trigger PHP warnings/failures before your InvalidArgumentException path runs.
Suggested fix
private function readOldPacketLength(string $binary, int $offset, int $lengthType): array
{
return match ($lengthType) {
- 0 => [ord($binary[$offset] ?? "\0"), $offset + 1],
- 1 => [(int)unpack('n', substr($binary, $offset, 2))[1], $offset + 2],
- 2 => [(int)unpack('N', substr($binary, $offset, 4))[1], $offset + 4],
+ 0 => isset($binary[$offset])
+ ? [ord($binary[$offset]), $offset + 1]
+ : throw new InvalidArgumentException('Truncated OpenPGP packet length.'),
+ 1 => ($offset + 2) <= strlen($binary)
+ ? [(int) unpack('n', substr($binary, $offset, 2))[1], $offset + 2]
+ : throw new InvalidArgumentException('Truncated OpenPGP packet length.'),
+ 2 => ($offset + 4) <= strlen($binary)
+ ? [(int)unpack('N', substr($binary, $offset, 4))[1], $offset + 4]
+ : throw new InvalidArgumentException('Truncated OpenPGP packet length.'),
default => throw new InvalidArgumentException('Indeterminate OpenPGP packet lengths are not supported.'),
};
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Pgp/ArmoredPublicKeyParser.php` around lines 209 - 216,
readOldPacketLength currently calls unpack() for old-format lengthType 1 and 2
without verifying the input contains the required bytes, which can cause PHP
warnings on truncated input; update readOldPacketLength (and reference
parsePackets behavior) to first validate that ($offset + 2) <= strlen($binary)
for lengthType 1 and ($offset + 4) <= strlen($binary) for lengthType 2 and throw
an InvalidArgumentException (or similar) if the bytes are missing, otherwise
proceed to unpack and return the length and new offset as before.
| if ($search !== '') { | ||
| $qualified = $this->parseQualifiedLookupSearch($search); | ||
| if ($qualified !== null) { | ||
| $identityLike = '%' . $qualified['identity'] . '%'; | ||
| $params = [$identityLike, $identityLike, $identityLike, $identityLike]; | ||
|
|
||
| if ($qualified['type'] === 'host') { | ||
| $sql .= " | ||
| WHERE ( | ||
| LOWER(u.username) LIKE LOWER(?) | ||
| OR LOWER(u.real_name) LIKE LOWER(?) | ||
| OR LOWER(COALESCE(k.user_id_string, '')) LIKE LOWER(?) | ||
| OR LOWER(COALESCE(k.label, '')) LIKE LOWER(?) | ||
| ) | ||
| "; | ||
| } else { | ||
| $sql .= " | ||
| WHERE LOWER(COALESCE(u.fidonet_address, '')) = LOWER(?) | ||
| AND ( | ||
| LOWER(u.username) LIKE LOWER(?) | ||
| OR LOWER(u.real_name) LIKE LOWER(?) | ||
| OR LOWER(COALESCE(k.user_id_string, '')) LIKE LOWER(?) | ||
| OR LOWER(COALESCE(k.label, '')) LIKE LOWER(?) | ||
| ) | ||
| "; | ||
| array_unshift($params, $qualified['network']); | ||
| } | ||
| } else { | ||
| $normalizedFingerprint = strtoupper(ltrim($search, '0x')); | ||
| $sql .= " | ||
| WHERE k.fingerprint = ? | ||
| OR LOWER(u.username) LIKE LOWER(?) | ||
| OR LOWER(u.real_name) LIKE LOWER(?) | ||
| OR LOWER(COALESCE(k.email, '')) LIKE LOWER(?) | ||
| OR LOWER(COALESCE(k.user_id_string, '')) LIKE LOWER(?) | ||
| OR LOWER(COALESCE(k.label, '')) LIKE LOWER(?) | ||
| "; | ||
| $like = '%' . $search . '%'; | ||
| $params = [$normalizedFingerprint, $like, $like, $like, $like, $like]; | ||
| } |
There was a problem hiding this comment.
Host-qualified lookups ignore the host and swallow email searches.
parseQualifiedLookupSearch() classifies any alice@example.com string as type=host, but both host branches only compare alice and never validate example.com. That makes alice@wrong-host return the same result as alice, and exact email queries stop using the k.email predicate entirely.
Also applies to: 275-281, 316-417
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/PgpKeyService.php` around lines 200 - 239, The host-qualified branch
built when parseQualifiedLookupSearch() returns $qualified with type 'host'
ignores the domain and drops the email predicate; update the SQL and params so
the host (domain) is validated and email searches still work: when
$qualified['type'] === 'host' add a domain check (e.g. compare LOWER(k.email)
ends-with or LOWER(SUBSTR(k.email, INSTR(...))) = LOWER($qualified['host']) or
use LIKE '%@' . $qualified['host']) and include the domain parameter in $params
alongside the existing identity-like params; similarly ensure the other branches
(the non-qualified fingerprint/email branch and the other occurrences
referenced) keep or reintroduce the k.email predicate and adjust param ordering
to match the new placeholders so exact email queries still match k.email while
host-qualified lookups validate the domain.
| if (filter_var($network, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false | ||
| || filter_var($network, FILTER_VALIDATE_IP) !== false) { | ||
| return [ | ||
| 'identity' => $identity, | ||
| 'network' => strtolower($network), | ||
| 'type' => 'host', | ||
| ]; |
There was a problem hiding this comment.
Don't accept arbitrary host/IP targets from user-controlled keyserver queries.
This branch turns identity@hostname and even identity@IP into live outbound requests, and the new public /keyserver route feeds raw search input straight into this service. That makes the feature an SSRF primitive against localhost, RFC1918, and link-local addresses. Restrict web-exposed lookups to FTN destinations (or an explicit allowlist), and if host-mode is kept for trusted CLI use, also reject private/reserved resolutions and redirects before issuing the request.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/PgpLookupService.php` around lines 564 - 570, The current branch in
PgpLookupService that returns a host-type when filter_var($network, ...) matches
allows web requests (via the public /keyserver route) to trigger arbitrary
outbound host/IP lookups — make the service refuse host-mode for public HTTP
requests and only allow FTN-style lookups or explicit allowlisted hosts: detect
caller context (e.g. PHP_SAPI or the request origin) and if the caller is a
web/public request, do not enter the host branch and instead return an error or
force FTN lookup; if you must retain host-mode for trusted CLI use, gate the
branch on a trusted-context check and harden validation of $network by rejecting
loopback, RFC1918/private, link-local and reserved IPs and any hostnames that
resolve to those addresses, and perform safe redirect checks before issuing
outbound requests (log and return a safe error on rejection).
| {% else %} | ||
| const interestEchoareas = null; | ||
| {% endif %} | ||
| window.currentUser = {{ current_user|json_encode(15)|raw }}; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for where current_user is set in routes that render compose.twig
rg -n -C5 'current_user' routes/web-routes.php routes/api-routes.php | head -80
# Also check if there's a dedicated method that prepares user data for templates
rg -n 'getCurrentUser|currentUser' src/Repository: awehttam/binkterm-php
Length of output: 4539
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect compose.twig around line 476 to confirm variable origin/type
compose_file="$(fd -a -t f 'compose\.twig$' templates | head -n 1 || true)"
echo "Compose file: $compose_file"
nl -ba "$compose_file" | sed -n '440,520p'
# 2) Inspect Template.php addGlobalVariables sanitization for current_user
nl -ba src/Template.php | sed -n '1,240p'
nl -ba src/Template.php | sed -n '160,220p'
nl -ba src/Template.php | sed -n '170,210p'
# 3) Inspect Auth::getCurrentUser for what it returns
nl -ba src/Auth.php | sed -n '120,260p'
# 4) Search for any other sanitization of current_user fields
rg -n "unset\\(\\$currentUser\\['" src/Template.php src/Auth.php src -S || true
rg -n "addGlobal\\('current_user'|addGlobal\\('currentUser'|current_user\\b" src -S || trueRepository: awehttam/binkterm-php
Length of output: 182
🏁 Script executed:
#!/bin/bash
set -euo pipefail
compose_file="$(fd -a -t f 'compose\.twig$' templates | head -n 1 || true)"
echo "Compose file: $compose_file"
cat -n "$compose_file" | sed -n '440,520p'
echo "---- src/Template.php (sanitization around current_user globals) ----"
# Show the portion where current_user is unset/sanitized and added as a Twig global
cat -n src/Template.php | sed -n '1,220p'
echo "---- src/Template.php (addGlobalVariables) ----"
cat -n src/Template.php | sed -n '80,220p'
echo "---- src/Template.php (unset password_hash) ----"
# Narrow further if it exists
rg -n "unset\\(\\$currentUser\\['password_hash'\\]\\)" -n src/Template.php || true
cat -n src/Template.php | sed -n '160,210p'
echo "---- src/Auth.php (getCurrentUser return shape) ----"
cat -n src/Auth.php | sed -n '130,260p'
echo "---- src/Auth.php (full getCurrentUser function header area) ----"
# Print a bit more around the function definition
rg -n "function getCurrentUser\\(" src/Auth.php || true
AUTH_LN="$(rg -n "function getCurrentUser\\(" src/Auth.php | head -n1 | cut -d: -f1 || true)"
if [[ -n "${AUTH_LN:-}" ]]; then
start=$((AUTH_LN-20))
end=$((AUTH_LN+220))
cat -n src/Auth.php | sed -n "${start},${end}p"
fi
echo "---- Search for any other sanitization of current_user ----"
rg -n "unset\\(\\$currentUser\\['" src/Template.php src/Auth.php src -S || true
rg -n "addGlobal\\('current_user'|addGlobal\\(\"current_user\"|addGlobal\\('currentUser'" src/Template.php src -S || trueRepository: awehttam/binkterm-php
Length of output: 23911
🏁 Script executed:
#!/bin/bash
set -e
# 1) Extract Auth::getCurrentUser implementation
rg -n "function getCurrentUser\\(" -n src/Auth.php
AUTH_LN="$(rg -n "function getCurrentUser\\(" src/Auth.php | head -n1 | cut -d: -f1)"
start=$((AUTH_LN-30))
end=$((AUTH_LN+220))
echo "---- src/Auth.php around getCurrentUser (lines ${start}-${end}) ----"
cat -n src/Auth.php | sed -n "${start},${end}p"
# 2) Search for what keys Auth::getCurrentUser returns (array construction patterns)
echo "---- token-like / secret-like key mentions in Auth.php ----"
rg -n "(password_hash|csrf_token|session|token|remember|api_key|secret|auth|jwt)" src/Auth.php || true
# 3) Search for additional sanitization of current_user anywhere else
echo "---- other sanitization of currentUser in src/Template.php ----"
cat -n src/Template.php | sed -n '150,220p'
rg -n "unset\\(\\$currentUser\\['" src/Template.php || true
echo "---- all occurrences of adding current_user global ----"
rg -n "addGlobal\\('current_user'|addGlobal\\(\"current_user\"" src/Template.php src -S || trueRepository: awehttam/binkterm-php
Length of output: 21640
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect validateSession in src/Auth.php (what fields are returned as the user array)
rg -n "function validateSession\\(" src/Auth.php
VAL_LN="$(rg -n "function validateSession\\(" src/Auth.php | head -n1 | cut -d: -f1)"
start=$((VAL_LN-40))
end=$((VAL_LN+140))
echo "---- src/Auth.php around validateSession (lines ${start}-${end}) ----"
cat -n src/Auth.php | sed -n "${start},${end}p"
# 2) Search for how window.currentUser is used
compose_file="$(fd -a -t f 'compose\.twig$' templates | head -n 1)"
echo "---- usage of window.currentUser in compose.twig ----"
rg -n "window\\.currentUser|currentUser\\." "$compose_file"
echo "---- usage of currentUser in JS under web root (best-effort) ----"
# Try to limit scope; only inspect likely JS files referenced by compose.twig
# (avoid large search)
fd -t f -e js js templates | head -n 2 >/dev/null 2>&1 || true
rg -n "window\\.currentUser|currentUser\\." -S js src templates | head -n 200 || true
# 3) List keys present in the current_user array after Template.php sanitization (heuristic)
# Can't execute PHP; instead extract selected column names from validateSession SQL
echo "---- columns selected in validateSession SQL ----"
rg -n "SELECT[\\s\\S]*FROM user_sessions|FROM user_sessions s" -n src/Auth.php || trueRepository: awehttam/binkterm-php
Length of output: 10278
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Find where compose.twig is rendered
rg -n "compose\\.twig" routes src templates || true
# 2) Inspect the relevant part of routes/web-routes.php around where templateVars set current_user
rg -n "templateVars = \\[" routes/web-routes.php
# show around the previously found lines (approx 1725-1760)
cat -n routes/web-routes.php | sed -n '1710,1765p'
# 3) Inspect how templates are rendered (Template class render/response path)
rg -n "twig->render\\(|render\\(" src/Template.php src/WebDoorController.php routes/web-routes.php || true
# show Template.php render-related methods
rg -n "function .*render" src/Template.php || true
cat -n src/Template.php | sed -n '220,420p'
# 4) Check if render merges templateVars over globals or if it uses globals only
rg -n "addGlobal\\(|getTemplate|render\\(" src/Template.php || trueRepository: awehttam/binkterm-php
Length of output: 21957
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect route around compose.twig render
cat -n routes/web-routes.php | sed -n '1860,1950p'
# Inspect Template.php renderResponse to see whether variables override globals as expected
rg -n "function renderResponse\\(" -n src/Template.php
RENDER_LN="$(rg -n "function renderResponse\\(" src/Template.php | head -n1 | cut -d: -f1)"
start=$((RENDER_LN-30))
end=$((RENDER_LN+80))
cat -n src/Template.php | sed -n "${start},${end}p"Repository: awehttam/binkterm-php
Length of output: 8720
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the compose route block (around the line we saw rendering compose.twig)
rg -n "compose\\.twig" routes/web-routes.php
# Print more context above the renderResponse call
cat -n routes/web-routes.php | sed -n '1650,1935p'
echo "---- Inspect RouteHelper::requireAuth ----"
rg -n "function requireAuth\\(" src/RouteHelper.php src -S || true
# Show the definition
RB_LN="$(rg -n "function requireAuth\\(" src/RouteHelper.php | head -n1 | cut -d: -f1)"
if [[ -n "${RB_LN:-}" ]]; then
start=$((RB_LN-40)); end=$((RB_LN+120))
cat -n src/RouteHelper.php | sed -n "${start},${end}p"
fi
echo "---- Search for any sanitization of password_hash before passing templateVars['current_user'] ----"
rg -n "password_hash" routes/web-routes.php routes/api-routes.php src/Template.php src/RouteHelper.php src/Auth.php || trueRepository: awehttam/binkterm-php
Length of output: 18589
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect RouteHelper::requireAuth implementation
cat -n src/RouteHelper.php | sed -n '1,140p'
# Also inspect Auth.php validateSession return usage (ensure no sanitization elsewhere)
rg -n "password_hash" src/RouteHelper.php src/Auth.php src/Template.php || trueRepository: awehttam/binkterm-php
Length of output: 4367
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "password_hash" routes/web-routes.php templates src | head -n 200
rg -n "unset\\(\\$.*password_hash\\)" routes/web-routes.php src templates || trueRepository: awehttam/binkterm-php
Length of output: 1656
Sanitize current_user before exposing it as window.currentUser
templates/compose.twig assigns window.currentUser from the template variable current_user, which is passed from routes/web-routes.php as $templateVars['current_user'] = $user.
That $user originates from Auth::validateSession() and includes u.password_hash (and also email, about_me, last_login, etc.). While src/Template.php unsets password_hash for the Twig global current_user, it is bypassed here because the route-provided context variable current_user takes precedence.
Remove password_hash (and any other sensitive fields) from the user payload before assigning it to $templateVars['current_user'] / before rendering compose.twig (e.g., return a “safe user” DTO from Auth, or explicitly unset sensitive keys in the compose route).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@templates/compose.twig` at line 476, The template exposes full user data via
window.currentUser (window.currentUser = {{ current_user|json_encode(15)|raw
}}), which can leak sensitive fields from Auth::validateSession() such as
password_hash; fix by ensuring the route that sets $templateVars['current_user']
(in routes/web-routes.php) supplies a sanitized/safe-user DTO or explicitly
unsets sensitive keys before rendering compose.twig (e.g., remove password_hash,
email, last_login, about_me as needed) so compose.twig only receives
non-sensitive fields; alternatively modify the Auth call that returns the user
to return a sanitized array/object for use in templates.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@routes/api-routes.php`:
- Around line 12961-12962: The fallback for QR generation currently uses
$node['public_key'] ?? $node['node_id'] which only handles null; change the
logic so an empty string is treated as missing by checking and normalizing
$node['public_key'] (e.g., trim and is_empty) before assignment: set $publicKey
to $node['node_id'] when $node['public_key'] is null or an empty/whitespace
string, then call $service->getQrCodeSvg((string)$handle, (string)$publicKey).
In `@tests/explain_echomail_list_queries.php`:
- Around line 133-139: The filter SQL fragments and their bound params are
appended in different orders: buildEchomailIgnoreFilter('em') SQL is
concatenated to $filterClause before buildModerationVisibilityFilter('em') SQL,
but when assembling $pageParams/$totalParams/$unreadParams you place
$filterParams (which currently contains moderationFilter['params']) before
ignoreFilter['params'], causing mismatched bindings; to fix, ensure the params
are merged in the same order as the SQL by appending ignoreFilter['params']
first then moderationFilter['params'] when building $filterParams and when
merging into $pageParams, $totalParams and $unreadParams so the parameter order
matches the order of $filterClause (refer to buildEchomailIgnoreFilter,
buildModerationVisibilityFilter, $filterClause, $filterParams, $pageParams,
$totalParams, $unreadParams).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 0dd306eb-ee08-40f1-b230-8221c0bb2e2a
📒 Files selected for processing (10)
database/migrations/v20260606034151_add_meshcore_node_adverts.sqldocs/PacketBBS.mdpublic_html/js/meshcore-node-info.jspublic_html/webdoors/cwn/api.phproutes/api-routes.phproutes/packetbbs-routes.phproutes/web-routes.phpsrc/PacketBbs/MeshcoreAdvertService.phpsrc/PacketBbs/PacketBbsNodeService.phptests/explain_echomail_list_queries.php
✅ Files skipped from review due to trivial changes (1)
- docs/PacketBBS.md
🚧 Files skipped from review as they are similar to previous changes (1)
- routes/web-routes.php
| $publicKey = $node['public_key'] ?? $node['node_id']; | ||
| echo $service->getQrCodeSvg((string)$handle, (string)$publicKey); |
There was a problem hiding this comment.
Treat blank public_key as missing before generating the QR.
This only falls back on null. If an existing/backfilled MeshCore row has public_key = '', the QR will encode an empty key instead of the promised node_id fallback, so the deep link is broken for that node.
Suggested fix
- $publicKey = $node['public_key'] ?? $node['node_id'];
- echo $service->getQrCodeSvg((string)$handle, (string)$publicKey);
+ $publicKey = trim((string)($node['public_key'] ?? ''));
+ if ($publicKey === '') {
+ $publicKey = (string)$node['node_id'];
+ }
+ echo $service->getQrCodeSvg((string)$handle, $publicKey);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $publicKey = $node['public_key'] ?? $node['node_id']; | |
| echo $service->getQrCodeSvg((string)$handle, (string)$publicKey); | |
| $publicKey = trim((string)($node['public_key'] ?? '')); | |
| if ($publicKey === '') { | |
| $publicKey = (string)$node['node_id']; | |
| } | |
| echo $service->getQrCodeSvg((string)$handle, $publicKey); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@routes/api-routes.php` around lines 12961 - 12962, The fallback for QR
generation currently uses $node['public_key'] ?? $node['node_id'] which only
handles null; change the logic so an empty string is treated as missing by
checking and normalizing $node['public_key'] (e.g., trim and is_empty) before
assignment: set $publicKey to $node['node_id'] when $node['public_key'] is null
or an empty/whitespace string, then call $service->getQrCodeSvg((string)$handle,
(string)$publicKey).
| $ignoreFilter = $handler->buildEchomailIgnoreFilter($userId, 'em'); | ||
| $filterClause .= $ignoreFilter['sql']; | ||
| $moderationFilter = $handler->buildModerationVisibilityFilter($userId, 'em'); | ||
| $filterClause .= $moderationFilter['sql']; | ||
| foreach ($moderationFilter['params'] as $param) { | ||
| $filterParams[] = $param; | ||
| } |
There was a problem hiding this comment.
Parameter binding order mismatch for filter clauses.
In buildFilterState, ignoreFilter['sql'] is appended to filterClause before moderationFilter['sql'] (lines 134, 136). However, when building $pageParams (line 219), $filterParams (containing moderationFilter['params']) is placed before $ignoreFilter['params'].
This causes parameter order to be incorrect when both filters produce placeholders, leading to wrong bind values in EXPLAIN output.
Proposed fix
function buildFilterState(MessageHandler $handler, PDO $db, int $userId, string $filter): array
{
$filterClause = '';
$filterParams = [];
+ $ignoreParams = [];
if ($filter === 'unread') {
$filterClause = ' AND mrs.read_at IS NULL';
} elseif ($filter === 'read') {
$filterClause = ' AND mrs.read_at IS NOT NULL';
} elseif ($filter === 'tome') {
$user = fetchOne($db, 'SELECT username, real_name FROM users WHERE id = ?', [$userId]);
if ($user) {
$filterClause = ' AND (LOWER(em.to_name) = LOWER(?) OR LOWER(em.to_name) = LOWER(?))';
$filterParams[] = $user['username'];
$filterParams[] = $user['real_name'];
}
} elseif ($filter === 'saved') {
$filterClause = ' AND sav.id IS NOT NULL';
}
$filterClause .= " AND (em.date_written IS NULL OR em.date_written <= (NOW() AT TIME ZONE 'UTC'))";
$ignoreFilter = $handler->buildEchomailIgnoreFilter($userId, 'em');
$filterClause .= $ignoreFilter['sql'];
+ $ignoreParams = $ignoreFilter['params'];
$moderationFilter = $handler->buildModerationVisibilityFilter($userId, 'em');
$filterClause .= $moderationFilter['sql'];
foreach ($moderationFilter['params'] as $param) {
$filterParams[] = $param;
}
return [
'filterClause' => $filterClause,
'filterParams' => $filterParams,
- 'ignoreFilter' => $ignoreFilter,
+ 'ignoreParams' => $ignoreParams,
];
}Then update the param merging:
$filterState = buildFilterState($handler, $db, $userId, $filter);
$filterClause = $filterState['filterClause'];
$filterParams = $filterState['filterParams'];
-$ignoreFilter = $filterState['ignoreFilter'];
+$ignoreParams = $filterState['ignoreParams'];
-$pageParams = array_merge($pageParams, $echoareaIds, $filterParams, $ignoreFilter['params'], [$limit, $offset]);
+$pageParams = array_merge($pageParams, $echoareaIds, $filterParams, $ignoreParams, [$limit, $offset]);And similar updates for $totalParams and $unreadParams.
Also applies to: 189-192, 218-219
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/explain_echomail_list_queries.php` around lines 133 - 139, The filter
SQL fragments and their bound params are appended in different orders:
buildEchomailIgnoreFilter('em') SQL is concatenated to $filterClause before
buildModerationVisibilityFilter('em') SQL, but when assembling
$pageParams/$totalParams/$unreadParams you place $filterParams (which currently
contains moderationFilter['params']) before ignoreFilter['params'], causing
mismatched bindings; to fix, ensure the params are merged in the same order as
the SQL by appending ignoreFilter['params'] first then
moderationFilter['params'] when building $filterParams and when merging into
$pageParams, $totalParams and $unreadParams so the parameter order matches the
order of $filterClause (refer to buildEchomailIgnoreFilter,
buildModerationVisibilityFilter, $filterClause, $filterParams, $pageParams,
$totalParams, $unreadParams).
No description provided.