feat: Wikitongues Download Gateway — sub-phases 0–3#560
Merged
FredericoAndrade merged 14 commits intomainfrom Mar 14, 2026
Merged
feat: Wikitongues Download Gateway — sub-phases 0–3#560FredericoAndrade merged 14 commits intomainfrom
FredericoAndrade merged 14 commits intomainfrom
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
download-gatewayplugin (sub-phase 0): activation/deactivation/uninstall hooks,GATEWAY_ENABLEDfeature flag, Logger, Settings admin page placeholderdbDelta()(sub-phase 1):wp_gateway_tokens,wp_gateway_people,wp_gateway_download_events,wp_gateway_webhook_deliverySettingsRepository,PolicyResolver(per-resource → taxonomy → global precedence),EventBus(namespaced WP hooks),DownloadEventRepositoryGET /wp-json/gateway/v1/download/{post_id|token}— issues signed tokens, setsgateway_vidvisitor cookie, logs click + redirect events, resolves file URL viaFileResolverRegistry, returns 302 to fileDocumentFileResolverhandlesdocument_filesposts via ACFfilefield. Additional CPT resolvers (videos, captions) register the same interface in sub-phase 6.Endpoint is gated behind
define('GATEWAY_ENABLED', true)inwp-config.php— admin UI registers regardless.Test plan
define('GATEWAY_ENABLED', true)towp-config.phpcurl -v "http://localhost:8888/wikitongues/wp-json/gateway/v1/download/{document_files_post_id}"— expect 302 withLocationheader pointing to the PDF andSet-Cookie: gateway_vid=...wp_gateway_tokens(token + expiry) andwp_gateway_download_events(click + redirect events)composer test -- --testsuite Gateway— 53 tests, all passing🤖 Generated with Claude Code