Skip to main content

AccountController

\Benecaster\REST\AccountController

Class Free

Subscriber-facing account REST endpoints, all under /wp-json/benecaster/v1/account/*. Every route uses cookie auth (permission_callback_subscriber — is_user_logged_in(), no nonce) with one deliberate exception: GET /account/verify-email is __return_true because the person clicking an emailed link may not be carrying the session that requested the change, and the single-use hashed token in the link is the actual credential. Covers subscriber-editable profile fields (name, password, email-change verification), avatar upload/removal, Supporter Wall opt-in/message/display-name, the subscriber’s own subscription list, feed-token reset, a QR-code value for the show’s public URL, and a GDPR data export. Registered via register_routes() on rest_api_init; not intended for direct instantiation.

Constructor Dependencies

Type Description
\Benecaster\Token\TokenRepository Reads the subscriber's token rows for get_subscriptions(), reset_token(), get_qr_code().
\Benecaster\Token\TokenManager Performs the actual token reset in reset_token().
\Benecaster\Show\TierMapRepository Resolves a token's tier_slug to its human-readable internal tier name.
\Benecaster\Avatar\AvatarManager Reads/writes the subscriber's avatar attachment ID and resolves its URL.
\Benecaster\SupporterWall\SupporterWallManager Reads and writes Supporter Wall visibility and message opt-in state.
\Benecaster\Email\EmailManager Sends the email-change verification message.

Methods

Method Visibility Since Description
register_routes(): void Public — Registers every route below on rest_api_init. GET /account/subscriptions, POST /account/reset-token, GET /account/qr-code, GET /account/export, POST+DELETE /account/avatar, POST /account/supporter-wall-visibility, POST /account/wall-message, POST /account/wall-name, GET+POST /account/profile — all permission_callback_subscriber. GET /account/verify-email is the one public route (permission_callback '__return_true'), with a required=false string 'token' arg.
get_subscriptions( WP_REST_Request $request ): WP_REST_Response|WP_Error Public — GET /account/subscriptions. Optional show_id query param scopes to one show's active token; otherwise returns every active token for the subscriber. For each, resolves the show post, the tier's internal name via TierMapRepository, a masked feed URL via SubscriberFeedUrl::masked(), and the show artwork URL. Skips any token whose show post no longer exists.
reset_token( WP_REST_Request $request ): WP_REST_Response|WP_Error Public — POST /account/reset-token. Body: show_id. 400 rest_invalid_param when missing; 404 when there is no active token for the (user, show) pair. Delegates the reset to TokenManager::reset(), returns the new feed_url, an 8-character token_prefix, and deep-link app_links built via AppDeepLinks::for_feed_url(). Sets Cache-Control: no-store on the response — the only point the plaintext token exists outside TokenManager is between reset() and this return.
get_qr_code( WP_REST_Request $request ): WP_REST_Response|WP_Error Public — GET /account/qr-code. Requires show_id and an active token for that show; 404 otherwise. Returns the show's public permalink for client-side QR rendering — NOT the private feed URL. The full feed token is never persisted (only its 8-character prefix), so a QR code for the private feed can only be generated client-side immediately after a token reset.
get_export( WP_REST_Request $request ): WP_REST_Response|WP_Error Public — GET /account/export. GDPR right-of-access export: account fields (email, display name, registration date), every token record regardless of status, migration patron records (when that table exists), and up to 1000 most-recent download-log rows. Nothing is written server-side — the client triggers a Blob download from the JSON response. Excludes token hashes (only the 8-char prefix) and download_log's ip_hash (an internal dedup key, not subscriber-meaningful data).
upload_avatar( WP_REST_Request $request ): WP_REST_Response|WP_Error Public — POST /account/avatar, multipart field avatar. Validates presence, upload error state, size (<= AVATAR_MAX_BYTES, 2MB), MIME type against AVATAR_ALLOWED_MIMES via wp_check_filetype_and_ext(), and that the image is exactly square (rejects with 422 benecaster_avatar_not_square otherwise — no server-side cropping). Sideloads via wp_handle_upload(), inserts a media-library attachment owned by the subscriber, generates attachment metadata, stores the attachment ID on AvatarManager::META_KEY user meta, and deletes the subscriber's previous avatar attachment if one existed (so repeat uploads do not accumulate media-library orphans). Fires benecaster_avatar_uploaded on success.
delete_avatar( WP_REST_Request $request ): WP_REST_Response|WP_Error Public — DELETE /account/avatar. Clears the avatar user meta and deletes the underlying attachment, reverting the subscriber to the default WordPress avatar (Gravatar, or the benecaster_avatar_fallback result when Gravatar is off site-wide). Idempotent — returns 200 {removed: false} when no avatar was set, rather than an error. Fires benecaster_avatar_deleted only when an avatar actually existed.
set_supporter_wall_visibility( WP_REST_Request $request ): WP_REST_Response|WP_Error Public — POST /account/supporter-wall-visibility. Body: visible (bool). Delegates to SupporterWallManager::set_subscriber_visible(). Writes unconditionally on every call, even when the value is unchanged from the stored one — idempotent from the subscriber's perspective, but re-confirms are not distinguishable from changes in this method.
set_supporter_wall_message( WP_REST_Request $request ): WP_REST_Response|WP_Error Public — POST /account/wall-message. Body: message. 403 rest_forbidden when none of the subscriber's shows have the show-level "why field" toggle on — prevents a message written under one show's off state from auto-publishing if a different show later turns the field on. Sanitization (sanitize_textarea_field()) and truncation to MESSAGE_MAX_LENGTH happen inside SupporterWallManager::set_subscriber_message(); an empty string is a valid explicit clear. Returns the post-sanitization stored value so the client does not need a follow-up GET.
update_wall_name( WP_REST_Request $request ): WP_REST_Response|WP_Error Public — POST /account/wall-name. Body: wall_name. 400 when empty after trimming or over 100 characters. Writes only WALL_NAME_META user meta — never touches wp_users.display_name, so the change has no site-wide effect. Consumed by [benecaster_supporter_wall], [benecaster_member_thanks], and the Episode Workbench member list via benecaster_resolve_wall_display_name(). Rate limited (bucket account_wall_name).
get_profile(): WP_REST_Response|WP_Error Public — GET /account/profile. Returns first_name, last_name, display_name, email, pending_email (via profile_payload()), and password_is_generated (whether GeneratedPasswordMarker marked this account's password as system-generated, so the form can hide the Current Password field).
update_profile( WP_REST_Request $request ): WP_REST_Response|WP_Error Public — POST /account/profile. Three independent, opt-in writes gated only by key presence: (1) names — first_name/last_name meta and display_name, written directly, no gate; (2) password — written only when non-empty; requires current_password UNLESS GeneratedPasswordMarker::is_generated() is true for this account (accounts created with a password nobody ever learns — follower email-first signup, bulk enrolment, manual add, the donation webhook — would otherwise be permanently locked out of setting one), validated by SignupPasswordPolicy, and forces a fresh auth cookie because wp_set_password() invalidates the requesting session too; (3) email — never written directly. A differing, available address starts start_email_verification() and the response reports email_pending: true rather than changing user_email immediately.
verify_email( WP_REST_Request $request ): WP_REST_Response Public — GET /account/verify-email?token=. Public route. Splits the token into {user_id}:{secret}, hashes the secret and compares against the stored hash, and checks the UTC expiry. On success, promotes PENDING_EMAIL_META to wp_users.user_email, clears both pending-email meta keys, fires benecaster_email_changed, and redirects to the account page with ?bc_email_verified=1. Always returns a 302 redirect (never JSON) with Cache-Control: no-store, since this route is reached by a human clicking a mail-client link. A replayed link (well-formed token, real user, no pending change left) redirects with bc_email_error=already_verified; a garbled, unknown, mismatched, or expired one redirects with bc_email_error=invalid_token — an expired link therefore reads as invalid, not as already-verified. Re-checks that the pending address is still unclaimed at consumption time (someone else may have registered it during the 24-hour window) and fails with bc_email_error=email_exists if not.
active_pending_email( int $user_id ): string — — Returns the pending new-email address, or '' when none is in flight or its verification link has expired. Public and static so the [benecaster_account] shortcode can render the same "waiting for confirmation" state without duplicating the expiry rule.

Constants

Name Value Description
AVATAR_MAX_BYTES 2097152 Maximum accepted avatar upload size in bytes (2 MB).
AVATAR_ALLOWED_MIMES ['jpg|jpeg|jpe' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp'] Accepted avatar file extensions mapped to their required MIME type, passed to wp_check_filetype_and_ext() and wp_handle_upload().
WALL_NAME_META '_benecaster_wall_display_name' User meta key for the subscriber's chosen Supporter Wall display name. Separate from wp_users.display_name.
PENDING_EMAIL_META '_benecaster_pending_email' User meta key holding the address a subscriber has asked to change to, unverified. wp_users.user_email is untouched until verification succeeds.
PENDING_EMAIL_TOKEN_META '_benecaster_pending_email_token' User meta key holding 'sha256(secret)|expiry' (UTC Y-m-d H:i:s) for the in-flight email verification link.
PENDING_EMAIL_TTL DAY_IN_SECONDS How long an emailed verification link stays valid.

Hooks Fired

Notes

Extends RestController, which supplies register() (hooks register_routes() onto rest_api_init), the permission_callback_admin / permission_callback_editor / permission_callback_subscriber callbacks (all delegating to RestPermissions), rest_error(), not_found(), and non_production_billing_error(). Add-ons should not extend RestController or this class directly — register independent routes on rest_api_init and reuse the benecaster_rest_permission_admin() public function instead, per the base class's own docblock.

benecaster_email_changed fires only on a verified email change, never on the request to change it — a listener syncing the address to an external list would otherwise sync an address the subscriber may have mistyped.