Protected Downloads Runtime and API

The Protected Downloads module combines a dedicated download post type, access-checked delivery, optional provider-backed remote files, post/structure assignment reads, and direct-user enrollment access for Bricks layouts. Remote downloads can use the default PHP proxy or an authorized short-lived signed-URL redirect. The canonical runtime owner is src/Modules/Downloads/DownloadRuntime.php. Since version 1.1.12, assigned-download reads include both post-level and structure-level assignment scopes.

Module Gate and Entry Points

  • Module flag: brm_enable_protected_downloads
  • Bootstrap path: src/Bootstrap/BootstrapCoordinator.phpDownloadRuntime::boot()
  • Admin page: admin.php?page=brm-downloads
  • Bricks element: src/Elements/ProtectedDownloadElement.php
  • Runtime request adapter: src/Modules/Downloads/DownloadRuntime.php
  • Delivery response owner: src/Modules/Downloads/DownloadDeliveryService.php
  • Remote proxy owner: src/Modules/Downloads/DownloadRemoteProxyService.php
  • Download settings owner: src/Modules/Downloads/DownloadSettingsService.php
  • Authorized delivery snapshot read owner: src/Modules/Downloads/DownloadAuthorizedDeliverySnapshotService.php
  • Post/structure assignment owner: src/Modules/Downloads/DownloadAssignmentService.php
  • Download mutation owner: src/Services/DownloadMutationService.php
  • Concurrent mutation guard: src/Services/ConcurrencyGuardService.php
  • Fail-closed access reader: src/Modules/Downloads/DownloadAccessReader.php
  • Direct-user coordinator: src/Modules/Downloads/DownloadRecipientAccessService.php
  • Enrollment-row write owner: src/Services/EnrollmentService.php

Core Runtime Responsibilities

  • register the brm_download post type
  • add rewrite rules and handle secure download requests
  • boot provider support and the remote file browser
  • register meta boxes and save download metadata
  • serve local files, proxy remote bytes, or hand an authorized remote GET to a capable provider
  • create the optional download logs table

Private Local Storage Provisioning

PrivateStorageService is the canonical resolver and setup/read owner for local protected files. Enabling Protected Downloads follows AdminSettingsService::save_module_state()run_module_toggle_side_effects()PrivateStorageService::get_storage_diagnostics( 'downloads' ). The service first tries the explicitly configured BRM_PRIVATE_STORAGE_DIR, then the deterministic brm-private directory beside the server’s public DOCUMENT_ROOT. It creates the component directory, rejects symlinks, verifies containment and non-web accessibility after mutation, hardens permissions, and writes server deny files.

Downloads are a strict, durable component. Public wp-content/uploads fallbacks and temporary storage are never accepted for local download delivery. If the server cannot prove or create the private root, the module-enable response includes setup_required and the admin redirects to admin.php?page=brm-downloads&tab=storage-providers. That screen uses the canonical target derived from DOCUMENT_ROOT, or from ABSPATH only to generate the explicit configuration recommendation when the document root is unavailable, and provides an exact BRM_PRIVATE_STORAGE_DIR line plus a verification action. Runtime upload and delivery remain fail-closed until diagnostics report an available private directory.

Provider Architecture

Remote providers register through src/Modules/Downloads/Providers/ProviderRegistry.php. The current provider set includes Local, S3, Bunny, and GCS adapters. Proxy mode keeps the provider’s signed origin URL server-side and relays validated ranges through DownloadRemoteProxyService. Direct mode is opt-in: S3, Bunny, and GCS return true from supports_direct_delivery(), while Local and custom providers remain proxy-only unless they explicitly implement the browser-facing signed-URL contract and allowed redirect hosts.

Remote Delivery Settings

DownloadSettingsService is the canonical settings owner. The admin mutation path is brm_save_download_settingsDownloadsAdminHandlers::ajax_save_download_settings()DownloadSettingsService::save_from_post(). It stores brm_download_remote_delivery_mode as proxy or direct and brm_download_remote_proxy_max_file_size_mb as a bounded integer from 1 to 102400 MB. Defaults are proxy and 500 MB.

Each remote download can store _brm_download_delivery_mode as inherit, proxy, or direct. DownloadMutationService owns that write. DownloadAuthorizedDeliverySnapshotService resolves the effective mode while the download mutation guard is held, so delivery never rereads mutable mode or provider metadata after authorization.

DownloadDeliveryService redirects only a body-bearing GET with download intent, effective direct mode, and an explicitly capable provider. It validates the signed destination against the provider’s allowed hosts and emits the cross-host handoff through wp_safe_redirect(). Playback, stream intent, HEAD, local files, and explicit proxy overrides stay on the gateway/proxy path. The generated URL is capped at 3600 seconds and each provider’s existing Download URL Expiry setting can shorten that lifetime.

Proxy mode compares the resolved file size with the configured MB limit converted to bytes. The brm_protected_media_proxy_max_file_bytes filter remains the final developer override. Increasing the limit permits a larger transfer attempt but does not remove PHP worker, memory, execution-time, gateway, or upstream timeout limits. Direct mode is the scalable path for multi-gigabyte downloads because WordPress never handles the file bytes.

Assigned Downloads

DownloadAssignmentService owns the relation between content posts, content structures, and download posts. Post assignments remain repeated post meta under _brm_assigned_download_id. Since version 1.1.12, structure assignments are stored in per-structure options named brm_structure_assigned_downloads_{structure_id} with autoload disabled. The structure interface AJAX layer validates the editor request and delegates persistence to this service; it does not own download assignment storage.

The Assignable Post Types setting is part of the same assignment owner. The settings UI posts the complete checkbox map as brm_download_assignment_post_types through brm_save_download_settings; DownloadsAdminHandlers accepts either normal form arrays or JSON AJAX payloads and delegates persistence to DownloadAssignmentService::save_supported_post_type_map(). The stored option is brm_download_assignment_post_types with autoload disabled.

Render adapters use assignment reads in three places: ProtectedDownloadElement resolves selected/manual, current-user enrollment, current post, specific post, current structure, and specific structure sources; native Bricks Posts query loops use the assigned-download query controls for brm_download; and DownloadsDynamicTags resolves scalar assignment modifiers such as {brm_download:url:post} and {brm_download:filename:structure=course_slug}.

Direct-User Enrollment Access

Since version 1.4.0, direct-user access reuses the existing enrollment model. Standard mode keeps required-level behavior. Direct-user mode clears required levels, saves a required/current-post enrollment rule, refreshes its wp_brm_post_data projection, and synchronizes editor-owned enrollment rows with source=download_assignment and source_ref=download_editor. An empty editor-owned recipient set grants nobody by itself, although another enrollment source can still grant access. Direct assignment is capped at 200 recipients per download; larger audiences should use levels or automations.

The canonical write path is DownloadsAdminHandlersDownloadMutationServiceDownloadRecipientAccessServiceEnrollmentRequirementService, EnrollmentRequirementProjectionService, and EnrollmentService. Removed recipients are revoked rather than deleted; the coordinator never mutates enrollment sources owned by manual enrollment, payments, automations, or integrations.

Recipient activation and revocation reuse the normal enrollment access-granted/access-lost events. Enrollment Activated fires only when effective access changes from inactive to active; it does not fire when another enrollment source already grants access or when an unchanged recipient is saved again. For a “download ready” email, use the existing Enrollment Activated trigger with the workflow condition resource.post_type equals brm_download. Without that condition, the trigger also matches enrollments for other post types. Do not send mail from the download save request.

Safe Mutation and Repair

Existing-download saves are serialized by ConcurrencyGuardService under download_mutation:{download_id}. A 10-second contention timeout returns HTTP 409 with brm_download_mutation_busy before post state changes. During a protected mutation, DownloadMutationService also writes the persistent _brm_download_mutation_locked marker. DownloadAccessReader hard-denies normal and hybrid access while that marker exists, before the brm_can_access_download filter runs.

Direct-user saves revoke removed editor-owned recipients while locked, save and verify the access policy/projection plus file/storage and image metadata, remove and verify removal of the persistent marker, and only then activate added recipients. A failure before unlock leaves an existing download locked. Repair requires access_mode, recipient_ids, required_levels, storage_type, and image_id, plus file_path for local storage or both remote_key and remote_provider for remote storage. Partial or uploaded-file-only retries return brm_download_mutation_locked.

DownloadAuthorizedDeliverySnapshotService::get_authorized_delivery_snapshot() takes the same download_mutation:{download_id} guard with a zero-second wait, checks access, and captures one immutable local or remote delivery descriptor inside the critical section. The service releases the guard before handing the descriptor to DownloadRuntime, which remains the rate-limiting and logging request adapter and delegates response preparation to DownloadDeliveryService. Runtime delivery consumes the captured full path or remote key/provider ID and never rereads mutable file metadata, so busy and access errors fail closed instead of crossing into delivery. Headless current-user list and detail reads reuse the snapshot service for file_info; list errors omit the item and detail errors return no file data.

GET /wp-json/bricksmembers/v1/admin/downloads exposes the non-secret repair fields access_mode, recipient_ids, required_levels, storage_type, file_path, remote_key, remote_provider, delivery_mode, and image_id, plus the computed mutation_locked boolean. Member list/detail routes omit those admin-only fields. Provider credentials, download keys, and the raw _brm_download_mutation_locked meta key are never included.

Current-User Bricks Reads

The Protected Download element accepts download_source=current_user_enrollments. Native brm_download post loops accept brmAssignedDownloadsOnly=current_user_enrollments. Both delegate to DownloadRecipientAccessService, return only accessible published direct-mode downloads, and return an empty result for logged-out or unmatched viewers without falling back to recent downloads. The saved filter_by_level key remains backward-compatible, but the Builder label is now Hide Inaccessible Downloads.

Dynamic Tags and Assistant Metadata

DownloadsDynamicTags owns protected-download dynamic tag rendering. Download tags cover the secure URL/default output, explicit :url, link HTML, filename, formatted size, file type, image outputs, fixed download IDs, and since version 1.1.12, post/structure assignment modifiers. DynamicTagAssistantCatalog, DynamicTagAssistantTagGenerator, and DynamicTagAssistantTagDescriber describe and generate those tags for the Bricks Dynamic Tag Assistant, but runtime rendering stays in DownloadsDynamicTags.

Secure Delivery Flow

  1. resolve the requested download and logged-in user
  2. take download_mutation:{download_id} with zero wait
  3. check access and capture the complete local or remote delivery descriptor, including effective delivery mode, while guarded
  4. fail closed on a busy or access error
  5. release the guard, then rate-limit and log
  6. stream the captured local path, proxy validated remote ranges, or issue a validated provider redirect without rereading file metadata

Admin Surfaces

  • src/Admin/Pages/Routes/DownloadsPage.php
  • src/Admin/DownloadsAdmin.php
  • src/Admin/DownloadsAdminHandlers.php

These classes own the admin UI transport. They do not become alternate owners for runtime access rules or provider delivery.

The current downloads page uses assets/admin/js/downloads.js. The old duplicate page renderer and old downloads page script were removed when the final brm-downloads route became the only downloads admin surface, so there is no separate legacy settings script to keep in sync.

Edit Guidance

  • Start in DownloadRuntime for request handling, CPT wiring, or provider boot.
  • Start in PrivateStorageService for private-root resolution, setup diagnostics, containment checks, or strict local-storage availability.
  • Start in DownloadDeliveryService for proxy-versus-redirect response behavior.
  • Start in DownloadSettingsService for global delivery settings, proxy limits, or per-download mode resolution.
  • Start in DownloadAssignmentService for post/structure assignment persistence and reads.
  • Start in DownloadRecipientAccessService for direct-user access mode or recipient synchronization.
  • Start in the provider adapter for provider-specific signing, direct capability, or redirect-host behavior.
Get BricksMembers

Start Building Your Membership Site Today

Create, sell, and manage your content without limits. BricksMembers gives you everything you need to build membership and LMS sites with Bricks Builder.

Lifetime updates & bug fixes • Premium support • 0% transaction fees • 60-day money-back guarantee