Skip to content

feat: Wikitongues Download Gateway — sub-phases 0–3#560

Merged
FredericoAndrade merged 14 commits intomainfrom
feature/cc/download-gateway-phase-0
Mar 14, 2026
Merged

feat: Wikitongues Download Gateway — sub-phases 0–3#560
FredericoAndrade merged 14 commits intomainfrom
feature/cc/download-gateway-phase-0

Conversation

@FredericoAndrade
Copy link
Contributor

Summary

  • Scaffolds the download-gateway plugin (sub-phase 0): activation/deactivation/uninstall hooks, GATEWAY_ENABLED feature flag, Logger, Settings admin page placeholder
  • Creates four custom DB tables on activation via dbDelta() (sub-phase 1): wp_gateway_tokens, wp_gateway_people, wp_gateway_download_events, wp_gateway_webhook_delivery
  • Adds core primitives (sub-phase 2a): SettingsRepository, PolicyResolver (per-resource → taxonomy → global precedence), EventBus (namespaced WP hooks), DownloadEventRepository
  • Implements the download endpoint (sub-phase 3): GET /wp-json/gateway/v1/download/{post_id|token} — issues signed tokens, sets gateway_vid visitor cookie, logs click + redirect events, resolves file URL via FileResolverRegistry, returns 302 to file

DocumentFileResolver handles document_files posts via ACF file field. Additional CPT resolvers (videos, captions) register the same interface in sub-phase 6.

Endpoint is gated behind define('GATEWAY_ENABLED', true) in wp-config.php — admin UI registers regardless.

Test plan

  • Activate plugin — confirm 4 DB tables created in phpMyAdmin
  • Add define('GATEWAY_ENABLED', true) to wp-config.php
  • curl -v "http://localhost:8888/wikitongues/wp-json/gateway/v1/download/{document_files_post_id}" — expect 302 with Location header pointing to the PDF and Set-Cookie: gateway_vid=...
  • Confirm rows in wp_gateway_tokens (token + expiry) and wp_gateway_download_events (click + redirect events)
  • composer test -- --testsuite Gateway — 53 tests, all passing

🤖 Generated with Claude Code

FredericoAndrade and others added 14 commits March 14, 2026 20:10
Adds the plugin bootstrap with activation/deactivation/uninstall hooks,
DG_ENABLED feature flag constant, Logger class (info/error/debug levels),
environment requirement checks on activation, and a Settings admin page
placeholder under Settings → Download Gateway.

REST namespace is gateway/v1. No download interception yet — gateway is
disabled until DG_ENABLED is defined true in wp-config.php.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates four custom tables on activation via dbDelta() (idempotent):
- wp_dg_tokens: one-time signed download tokens with expiry
- wp_dg_people: email-known visitors with consent and anonymization flags
- wp_dg_download_events: full download funnel lifecycle (click → redirect)
- wp_dg_webhook_delivery: outbound webhook retry queue with dead-letter support

Schema version tracked in dg_schema_version option. Tables dropped cleanly
on plugin uninstall. Schema::create_tables() called from Activator::activate().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renames all constants (DG_* → GATEWAY_*), DB tables (wp_dg_* → wp_gateway_*),
WP options (dg_* → gateway_*), cookie name (dg_vid → gateway_vid), REST paths
(/dg/ → /gateway/), and action hooks (dg/download → gateway/download).

Updates plan.md to match. Plugin slug and PHP namespace unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the four primitives that sub-phase 3 (download endpoint) depends on:

- SettingsRepository: typed getters for gateway_global_gate_policy and
  gateway_retention_months with validated defaults
- PolicyResolver: three-tier precedence (per-resource postmeta → taxonomy
  term meta → global default); taxonomy tier is wired but inactive until
  sub-phase 4 ACF fields write term meta
- EventBus: namespaced wrapper over WP do_action/add_action; all hooks
  prefixed gateway/ (e.g. gateway/download/click)
- DownloadEventRepository: inserts rows into wp_gateway_download_events
  with sanitized inputs; returns inserted ID or false on failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
IpHasher normalizes (trim, lowercase, strip IPv6 zone ID) and SHA-256
hashes IP addresses for privacy-safe storage. hash_from_server() prefers
X-Forwarded-For (leftmost) over REMOTE_ADDR.

Adds gateway plugin constants to test bootstrap and creates
tests/unit/download-gateway/. 12 tests, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allows running only download-gateway tests via:
  composer test -- --testsuite Gateway

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TokenRepository handles CRUD on wp_gateway_tokens:
- create(): generates bin2hex(random_bytes(32)) token, inserts with TTL
- find_by_token(): returns row object or null
- mark_used(): stamps used_at on redemption
- is_valid(): pure function — checks used_at IS NULL and expires_at > now;
  accepts optional $now param for deterministic time-sensitive tests
- purge_expired(): deletes expired unused tokens for retention job

24 tests (12 IpHasher + 12 TokenRepository), all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the file resolution layer:
- FileResolver interface: resolve(post_id) → ?string, storage_type() → string
- DocumentFileResolver: reads ACF 'file' field (URL string); handles array
  return format defensively; storage_type = 'media'
- FileResolverRegistry: static post_type → resolver map; for_post() calls
  get_post_type() and dispatches to the right resolver; reset() for tests

DocumentFileResolver registered for 'document_files' at plugin bootstrap.
VideoResolver and future types register the same way (sub-phase 6).

35 gateway tests, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
VisitorId: generate() produces 32-char hex visitor IDs; from_cookies()
validates the gateway_vid cookie value (rejects non-hex, wrong length,
uppercase, non-string); set_cookie() is a side-effect wrapper.

DownloadController: REST endpoint GET /wp-json/gateway/v1/download/{id}
- Dispatches on 64-char hex token or numeric post ID
- post ID path: checks resolver, enforces policy, issues token, logs
  click event, resolves file URL, logs redirect event, returns URL
- token path: validates token (not used, not expired), marks used,
  resolves file URL, logs redirect event, returns URL
- resolve() accepts injected $cookies/$server — fully unit-testable
- handle() is an untested 5-line wrapper that calls wp_redirect()+exit

Adds WP_Error stub to test bootstrap. 53 gateway tests, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Download Gateway → Wikitongues Download Gateway
WT Airtable Sync → Wikitongues Airtable Sync

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
IpHasher was loaded in the test bootstrap but never added to the plugin
bootstrap, causing a fatal 500 on the download endpoint in production.
Also sorted requires into dependency order.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Records architectural decisions (leaf-node model, FileResolverRegistry,
documents/document_files CPTs), completed sub-phases with PR references,
test counts, manual test confirmation, and admin UI notes for sub-phases
8 (reporting) and 9 (retention management).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PHPCS: auto-fixed 97 violations (short array syntax, formatting) in test files.
PHPStan: added wt-airtable-sync and download-gateway to scan paths;
regenerated baseline to absorb runtime-constant and ACF function errors
consistent with existing theme suppressions (440 total, up from ~400).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ng in baseline

Add \Mockery\MockInterface&\wpdb type hints to resolve $prefix/$last_error/$insert_id
property access errors in test files. Suppress GATEWAY_ENABLED constant checks inline
(runtime constant overridden in wp-config.php). Remove 7 entries from baseline; keep
Mockery once()/times() chaining errors deferred to future baseline reduction pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@FredericoAndrade FredericoAndrade merged commit 03d46c8 into main Mar 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant