Skip to content
WP EngineDocumentation

Embargoed Content

Embargoed Content prevents embargoed posts from being published, exposed via queries, or read by unauthorized users until the embargo lifts. It enforces embargoes through two layers: publishing guards (prevent invalid states) and display and capability guards (hide embargoed content from queries and capability checks).

Access is decided in two stages. A bypass skips the guards entirely (for every embargoed post); a per-post exemption lifts only the one post that carries the granting meta. Anonymous visitors (logged out, user ID 0) are always denied — they can neither bypass nor be exempted.

Bypass — Helpers::can_user_bypass_guards() (skips both guard layers for all embargoed posts):

  • User’s ID is in the site-wide allowed-users setting (mediapress_embargoed_content_settings_allowed_users).
  • User holds a role in the site-wide allowed-roles setting (mediapress_embargoed_content_settings_allowed_roles).

Per-post exemption — Helpers::is_post_viewable_by_user() (applies to the single embargoed post carrying the meta):

  • Post author — the user authored the post.
  • Allowed users — the user’s ID is in the post’s allowed_users meta.
  • Allowed roles — the user holds a role in the post’s allowed_roles meta.
  • Allow everyone — the post’s allowed_all toggle is enabled (grants any logged-in user; anonymous visitors remain embargoed).

Publishing guards apply unconditionally — nobody can publish an embargoed post regardless of capability, bypass, or exemption.

  • Date — the post is embargoed until its UTC lift date (mediapress_embargoed_content_lift_date_utc) passes, after which the guards step aside.
  • Indefinitely — setting mediapress_embargoed_content_is_indefinitely to true restricts the post with no expiry. It can never be published, scheduled, or shown until the flag is cleared.

Embargoed attachments are flagged with a lock badge in the MediaPress media UI — on grid items, list items, and in the media info panel. This is a visual cue only; access is enforced by the display and capability guards.

The badge is registered by consuming two JavaScript extension points exposed by the core media package (documented under Filters/Actions → JavaScript → Filters in the Media docs):

  • mediaPress.media.mediaItem — HOC wrapping each grid/list item; reads mediaItem from props.
  • mediaPress.media.mediaInfo — HOC wrapping the info panel; the record isn’t passed as a prop, so the callback re-fetches it with useEntityRecord using the attachmentId prop (core-data caches it, so no extra request).

Both callbacks render the badge when the attachment’s mediapress_embargoed_content_is_embargoed field is true. The badge component and all positioning styles live in Embargoed Content — the Media package references the badge nowhere, keeping the dependency direction one-way (Embargoed Content → Media).

When an embargoed post is trashed, restored, or permanently deleted, the package propagates that operation to all attachments whose post_parent is the affected post. This keeps media consistent with its parent and prevents orphaned files from lingering after the parent is removed.

Action on embargoed post Effect on attachments WordPress action hook Condition
Trashed Attachments are moved to trash trashed_post EMPTY_TRASH_DAYS and MEDIA_TRASH are set
Restored from trash Trashed attachments are restored untrashed_post EMPTY_TRASH_DAYS and MEDIA_TRASH are set
Permanently deleted Attachments are permanently deleted before_delete_post Always

Maps each post type slug to the embargo supports it declares.

Default

[
'post' => [ 'self' ],
'page' => [ 'self' ],
]

Supported values for the supports array:

  • 'self' — the post type carries its own embargo meta.
  • 'parent' — the post type inherits its embargo from its post_parent (most-restrictive wins). Only the direct post parent is checked; parent types are not resolved recursively through other parent types.

Usage

add_filter( 'mediapress_embargoed_content_post_types', function ( $post_types ) {
$post_types['mediapress_custom_post_type'] = [ 'self' ];
return $post_types;
});

The revision and attachment post types are always appended as [ 'parent' ] after the filter runs and cannot be removed or overridden. They carry no embargo meta of their own at this time; their embargoed state is inherited entirely from their parent post.

Publishing guards block attempts to publish embargoed posts or write contradictory embargo metadata. They aim to cover as much of the standard publishing surface as possible: block editor, quick edit, REST API, WP-CLI, programmatic calls, and cron.

flowchart TD
    A[Publish / update attempt] --> B{Entry point}
    B -->|"<code>wp_insert_post()</code> / <code>wp_update_post()</code>"| C["<code>wp_insert_post_data</code>"]
    B -->|"REST POST / PUT <code>/wp/v2/posts</code>"| D["<code>rest_pre_insert_{post_type}</code>"]
    B -->|"<code>wp_publish_post()</code> / cron"| E["<code>publish_{post_type}</code>"]
    B -->|"<code>update_post_meta()</code> / <code>add_post_meta()</code>"| F["<code>update_post_metadata</code> /<br/><code>add_post_metadata</code>"]

    C --> G{Resulting status<br/><code>publish</code> or <code>future</code>,<br/>and embargo conflicts?}
    D --> H{Resulting status<br/><code>publish</code> or <code>future</code>,<br/>and embargo conflicts?}
    E --> I{Currently embargoed<br/>and old status != <code>publish</code>?}
    F --> J{Meta change creates<br/>contradictory state?}

    G -- Yes --> K[Downgrade to previous status<br/>or <code>draft</code>]
    G -- No --> L[Allow]
    H -- Yes --> M[Return <code>WP_Error</code> — block request]
    H -- No --> L
    I -- Yes --> N[Revert to previous status<br/>via <code>wp_update_post</code>]
    I -- No --> L
    J -- Yes --> O[Return <code>false</code> — block meta write]
    J -- No --> L

Display and capability guards hide embargoed content from queries and block unauthorized access via capability checks. Queries are filtered via pre_get_posts, while frontend singular views are protected via template_redirect.

flowchart TD
    A[Read attempt] --> B{Entry point}
    B -->|"<code>WP_Query</code> (frontend / admin)"| C["<code>pre_get_posts</code>"]
    B -->|"<code>user_can</code> / <code>current_user_can</code>"| D["<code>map_meta_cap</code>"]
    B -->|"Singular frontend view"| E["<code>template_redirect</code>"]

    C --> F{Query has <code>post__in</code>?}
    F -- Yes --> G["Diff embargoed IDs out of <code>post__in</code><br/>(<code>[0]</code> sentinel if empty)"]
    F -- No --> H[Append embargoed IDs<br/>to <code>post__not_in</code>]

    D --> I{cap in<br/><code>read_post</code> / <code>edit_post</code> /<br/><code>delete_post</code>?}
    I -- No --> J[Pass through unchanged]
    I -- Yes --> I2{Post embargoed?}
    I2 -- No --> J
    I2 -- Yes --> K{"Viewable by user?<br/>author, <code>allowed_users</code>,<br/><code>allowed_roles</code>, or <code>allowed_all</code><br/>anonymous always denied"}
    K -- Yes --> L[Grant capability]
    K -- No --> M["Return <code>['do_not_allow']</code>"]

    E --> N{Embargoed and<br/>not viewable by user?}
    N -- Yes --> O[404 + exit]
    N -- No --> P[Allow]
Entry point Guard Result when blocked
wp_insert_post(), wp_update_post() wp_insert_post_data Status forced to draft / previous
REST POST /wp/v2/posts[/{id}] rest_pre_insert_{post_type} 400 WP_Error
wp_publish_post(), WP-Cron auto-publish publish_{post_type} Reverted via wp_update_post
update_post_meta() / add_post_meta() on embargoed keys update_post_metadata / add_post_metadata Returns false, meta unchanged
WP_Query (frontend / admin) Display_Guards::exclude_embargoed_posts_from_queries_pre_get_posts Embargoed IDs excluded
Singular frontend view Display_Guards::redirect_embargoed_posts 404 + exit
user_can( 'read_post' / 'edit_post' / 'delete_post' ) Access_Control::restrict_user_meta_capabilities do_not_allow

Last updated: