# Audit Context — remoji 2.4 (top-13 support-forum implementation)

Implements/triages the top-13 wordpress.org support-forum requests. Plan (`plan.md`) passed CX review over 5 rounds. User approved full scope incl. #4a/4b/4c. Version 2.2 → 2.4. No commit follows (user commits manually); audit is the mandatory post-change gate.

## What changed (by forum item)

- **#6** `GUI::reaction_bar()` now bails on `is_admin() || is_feed() || doing_filter('get_the_excerpt')` — no reaction bar / "Error happened" in excerpts/feeds.
- **#8** REST nonce localized/sent only for logged-in users (`GUI::enqueue_scripts`, `remoji.js`); guests go nonce-less so cached pages submit. Keeps logged-in identity intact (needed for `guest=false` + user_id).
- **#1** `remoji.js` gains an AJAX `error` callback surfacing `xhr.responseJSON.message` (e.g. `rest_no_route`) as text; readme FAQ documents the WAF/security-plugin root cause. Not claimed fully fixed (env-specific).
- **#10** `Util::pagination()` `'text-domain'` → `'remoji'`.
- **#9** Mobile: CSS `@media(max-width:600px)` pins `#remoji_panel` to viewport; `remoji.js` skips inline `left` under 600px; popover template `max-width:352px` → `100%`.
- **#5** New `reaction_disabled_types` config. Enforced BOTH in render (`reaction_bar`/`bbp_reaction_bar`) and server-side in `Reaction::add()` via `_reactable_target()`, which also requires published post / approved comment on a published parent (blocks orphan/hidden targets + widget leakage). New Lang: `invalid_target`, `post_type_disabled`.
- **#11** `reaction_bar.tpl.php` wraps the add button in `if ( Conf::val('guest') || is_user_logged_in() )`.
- **#7** `[remoji]` shortcode + `do_action('remoji_reaction',$id,$type)` + `post_emoji_auto` toggle (off = no auto the_content append). Shared `GUI::render_bar()`.
- **#2** `emoji_whitelist` config: `GUI::emoji()` list-mode + `emoji_handy()` filter the picker; `Reaction::add()` enforces server-side; single-key `emoji()` stays unfiltered so already-stored reactions still render; `Admin::_clean_key_list()` normalizes on save (trim/CRLF/dedupe/validate).
- **#12** `remoji_emoji_list` filter in `GUI::init()` (dev extension point). Full upload UI intentionally deferred.
- **#13** `bbp_get_topic_content` / `bbp_get_reply_content` filters → `bbp_reaction_bar()`.
- **#4a** `reaction_withdraw` config (default on): owner (user_id for logged-in, IP for guest) re-click withdraws. `Reaction::add()` returns `{action:added|withdrawn, count, src}`; JS acts on it.
- **#4b** `reaction_notify` config (default off): `wp_mail` author on NEW reactions only; skips author self-reaction; per-`type:id` transient throttle (5 min).
- **#4c** `show_reactors` config (default off): `user_id` column added (idempotent 2.4 migration `remoji_update_2_4` + `history.sql`); `Reaction::reactors()` batch-resolves names (one history query + one user query, no N+1); GDPR forces it off; guests shown as "Guest"; names escaped in template.
- **#3** Not delivered (full Twemoji set needs thousands of licensed SVGs + a new picker). `remoji_emoji_list` filter is the extension path. Documented.

## Cross-cutting
- New config keys in `Conf::$_default_options`: `post_emoji_auto`, `reaction_disabled_types`, `emoji_whitelist`, `reaction_withdraw`, `reaction_notify`, `show_reactors`.
- DB: `user_id bigint` + index, added via existing `_db_updater`/`data.upgrade.func.php` idempotent ALTER; `history.sql` synced for fresh installs.
- Also fixed a latent bug: `_log_reaction` now guards `IP::geo()` returning `WP_Error` (was `unset()`-ing on a non-array).
- i18n: new strings use `__(...,'remoji')` + added to `lang/remoji.pot`.

## WPCS escaping pass (post-review, user ran phpcs WordPress standard)
- Fixed all 120 `WordPress.Security.EscapeOutput.OutputNotEscaped` errors across 10 files: `esc_html__`/`esc_html_e` for translated text, `esc_attr`/`esc_url`/`(int)`/`(float)` for attribute/URL/numeric interpolation, `wp_kses_post` for admin-notice + theme widget-wrapper + user-configured widget-template HTML, and rebuilt `GUI` `build_*` echoes + `entry.tpl`/`reaction_popover.tpl` sprintf/concatenations to escape each dynamic part.
- Two justified `phpcs:ignore` only: `Postview::show_num`/`show_num_in_theme` (admin `manage_options`-configured `postview_tpl` HTML whose `data-*` the counter JS needs) and `GUI::do_reaction` (returns `render_bar()` markup already escaped in `reaction_bar.tpl.php`).
- After the pass: `phpcs --sniffs=WordPress.Security.EscapeOutput,WordPress.NamingConventions.PrefixAllGlobals` reports 0 across the plugin.

## WPCS full pass (user asked to clean the whole plugin)
- Added `phpcs.xml.dist` (WordPress base + text_domain/prefixes) excluding sniffs that are false positives for this plugin's deliberate conventions: `.cls.php`/`.tpl.php` filenames, `_`-prefixed private members, `@since`-header doc style (not exhaustive @param), inline-comment periods, Yoda (not wp.org-required), `WordPress.DB.DirectDatabaseQuery` (custom `*_remoji_history` table), and `PrefixAllGlobals`/`GlobalVariablesOverride` for `tpl/*` (partials included in method scope).
- `phpcbf` auto-fixed formatting (spacing, alignment, `==`→`===` where type-safe, quotes, increments).
- Behavior-adjacent manual fixes (verify these): input reads now `sanitize_text_field(wp_unslash())` — `router.cls` `$_REQUEST[ACTION/NONCE/TYPE]`, `gui.cls` `$_GET['page']` (sanitize_key), `ip.cls` `$_SERVER['REMOTE_ADDR']` (still FILTER_VALIDATE_IP'd after); removed dead unreachable code in `Util::version_check()` (body was after an unconditional `return false;`); hoisted `$img = GUI::emoji($emoji)` out of the `if` condition in `Reaction::add()` (short-circuit + result unchanged); `in_array(..., true)` in `comment.cls`; renamed `class f` → `class F` (only ref `data.cls` `F::read()`; autoloader lowercases so file/loader unaffected; PHP class names case-insensitive); `strip_tags`→`wp_strip_all_tags`, `parse_url`→`wp_parse_url`, `date`→`gmdate`, `wp_redirect`→`wp_safe_redirect`.
- Justified `phpcs:ignore` (no behavior change): NonceVerification on public REST endpoints (`reaction.add`, `postview.count_num`) + router (own `wp_verify_nonce`) + read-only admin `$_GET`; PreparedSQL where only the internal table-name constant is interpolated and values are `$wpdb->prepare()`-bound; `the_views` community filter name; `file_get_contents` of a bundled local JSON.
- Result: `phpcs --standard=phpcs.xml.dist` reports 0/0 across the whole plugin (27 files).

## Verification
- `php -l` clean on all changed PHP/template files; `node --check` clean on `remoji.js`; phpcs 0/0 whole-plugin.

## Review asks
- Migration ordering: `Conf::init()` runs `conf_upgrade()` (→ `remoji_update_2_4`) on the first post-upgrade request before any reaction INSERT — confirm no path inserts `user_id` before the column exists.
- Confirm the withdraw ownership (user_id logged-in / IP guest) and the added/withdrawn response contract are consistent between `Reaction::add()` and `remoji.js`.
