---
title: Embargoed Content
description: Embargoed Content keeps embargoed posts unpublished, out of queries, and unreadable by unauthorized users until the embargo lifts.
url: https://developers.wpengine.com/docs/newsroom/mediapress/embargoed-content/
image: https://developers.wpengine.com/og/docs/newsroom/mediapress/embargoed-content.png
---

# 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).

## Who can view embargoed content

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.

## How an embargo is defined

- **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 media indicator

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](https://developers.wpengine.com/docs/newsroom/mediapress/embargoed-content/#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](https://developers.wpengine.com/docs/newsroom/mediapress/media/)):

- `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).

## Media lifecycle

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                                       |

> **Caution**
>
> Any plugin registering a `parent`-inheriting type via `mediapress_embargoed_content_post_types` must satisfy two conditions for attachment cleanup to work correctly:
>
> 1. Any hook that triggers or performs post deletion must run on `before_delete_post` or earlier, so the parent post still exists in the database when the package resolves its type and embargo state.
> 2. The function used to delete the post must itself fire `before_delete_post`.
>
> If either condition is not met, `get_post_type()` returns `false`, `is_post_embargoed()` cannot resolve the post, and attachment cleanup is silently skipped.

## Hooks

### `mediapress_embargoed_content_post_types`

> **Caution**
>
> This filter is for MediaPress internal use only and may change without notice.

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

**Default**

```php
[
  '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.

> **Caution**
>
> Two configurations are experimental and may not function as expected: setting both `self` and `parent` on the same post type, and using custom post types that have their own capabilities with `map_meta_cap` set to `false`.

**Usage**

```php
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.

> **Attachment file access**
>
> Attachments inherit embargo checks from their parent post, which blocks access via the Media Library, REST API, and `wp_ajax_get-attachment`. This does **not** prevent direct file access via URL: an unauthorized user who knows or guesses the file URL (`/wp-content/uploads/...`) can access the file directly.
>
> To make guessing harder, filenames are obfuscated for files uploaded while the package is active. Directory indexing **must** be disabled in your server configuration.

## Publishing guards

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.

> **Note**
>
> The meta hook is the backstop: even if a future-extended embargo date *did* reach `wp_postmeta` directly (for example, through `$wpdb->update()`), the publish-side hooks would still catch the next publish attempt.

## Display and capability guards

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`.

## Coverage matrix

> **Caution**
>
> Guards attempt to cover a number of publish and display surfaces but currently **do not** intercept all possible data access paths, including but not limited to direct database queries via `$wpdb`, `get_post()`, and frontend `get_posts()`.

| 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`                    |

## Was this helpful?

Thanks for the feedback!

```json
{"@context":"https://schema.org","@graph":[{"@type":"WebSite","@id":"https://developers.wpengine.com/#website","name":"WP Engine Product Documentation","url":"https://developers.wpengine.com/","description":"Resources for developing, deploying, and scaling on WP Engine.","inLanguage":"en-US","keywords":["API documentation","product documentation","headless WordPress","managed WordPress hosting"],"publisher":{"@id":"https://wpengine.com/#organization"}}]}
```
