Skip to content

Conversation

@jasonbahl
Copy link
Collaborator

@jasonbahl jasonbahl commented Nov 19, 2025

What bug does this fix? Explain your changes.

This PR fixes two related issues with media item/attachment queries and privacy:

Bug #1: Media Item Privacy Inheritance (#3438)

The Bug:
Media items with "inherit" status attached to draft parent posts were incorrectly accessible when queried by DATABASE_ID, but correctly private when queried by SLUG. This inconsistent behavior occurred because:

  • When querying by SLUG, the node_resolver->resolve_uri() path uses WordPress's WP_Query, which properly filters out attachments with draft parents
  • When querying by DATABASE_ID, the code directly loads the post and bypasses WordPress's query system, relying solely on the Model's is_private() check
  • The Model's is_private() method was treating all attachments as public, regardless of their parent's privacy status

Root Cause:
The Post::is_private() method in src/Model/Post.php had a blanket rule that all attachments are public (lines 323-325), which ignored the "inherit" status that should cause attachments to inherit privacy from their parent posts.

The Fix:
Modified Post::is_private() to check if an attachment has "inherit" status and a parent post. If so, it now checks the parent's privacy status and inherits it. This ensures consistent behavior across all ID type resolution paths (DATABASE_ID, SLUG, GLOBAL_ID, etc.) while preserving the documented behavior that attachments without parents or with published parents remain public.

Bug #2: Missing Attachments with 'publish' Status (#1999)

The Bug:
Media items (attachments) that plugins had changed from the default 'inherit' status to 'publish' status were not being found in queries, including featuredImage connections and direct mediaItems queries. This occurred because:

  • PostObjectConnectionResolver was hardcoding post_status='inherit' for all attachment queries
  • When plugins change attachment status to 'publish' (e.g., for media privacy management), queries with post_status='inherit' would miss these attachments
  • This caused featuredImage to return null even when an image existed, and mediaItems queries to miss attachments with 'publish' status

Root Cause:
The PostObjectConnectionResolver::prepare_query_args() method was setting post_status='inherit' specifically for attachments (line 209), which prevented queries from finding attachments with any other status, including 'publish' status set by plugins.

The Fix:
Modified PostObjectConnectionResolver::prepare_query_args() to use post_status='any' for attachments instead of hardcoding 'inherit'. This ensures queries can find attachments regardless of their status ('inherit', 'publish', etc.), while the Model's is_private() method handles privacy checks based on the attachment's parent and status.

How the Fixes Work Together:

  • Fix Make Readme Better #1 ensures attachments with 'inherit' status correctly inherit privacy from their parent posts
  • Fix JWT Authentication #2 ensures queries can find attachments regardless of their status, then Fix Make Readme Better #1's privacy check determines visibility
  • Together, they provide consistent behavior: queries find all relevant attachments, and privacy is correctly enforced based on parent status

Does this close any currently open issues?

Tests

Issue #3438 (Media Item Privacy Inheritance)

Issue #1999 (Missing Attachments with 'publish' Status)

  • Failing tests: [link to failing CI run - TBD]
  • Passing tests: [link to passing CI run - TBD]

Before/After Examples

Before (Inconsistent Behavior - Issue #3438):

# Query as subscriber (non-admin)
query {
  # Query by DATABASE_ID - incorrectly returns media item
  mediaById: mediaItem(id: "7815", idType: DATABASE_ID) {
    id
    databaseId
    slug
    status
  }
  # Query by SLUG - correctly returns null
  mediaBySlug: mediaItem(id: "3-2", idType: SLUG) {
    id
    databaseId
    slug
  }
}

# Result:
# {
#   "mediaById": { "id": "...", "databaseId": 7815, "slug": "3-2", "status": "inherit" },
#   "mediaBySlug": null
# }
# 
# Inconsistent behavior: accessible by DATABASE_ID but not by SLUG

After (Consistent Behavior - Issue #3438):

# Query as subscriber (non-admin)
query {
  # Query by DATABASE_ID - now correctly returns null
  mediaById: mediaItem(id: "7815", idType: DATABASE_ID) {
    id
    databaseId
    slug
    status
  }
  # Query by SLUG - still correctly returns null
  mediaBySlug: mediaItem(id: "3-2", idType: SLUG) {
    id
    databaseId
    slug
  }
}

# Result:
# {
#   "mediaById": null,
#   "mediaBySlug": null
# }
# 
# Consistent behavior: both queries correctly return null for draft parent attachments

Before (Missing Attachments - Issue #1999):

# Query as admin
query {
  post(id: "123", idType: DATABASE_ID) {
    featuredImage {
      node {
        id
        databaseId
        status
      }
    }
  }
  mediaItems(where: { id: 456 }) {
    nodes {
      id
      databaseId
      status
    }
  }
}

# Result (when attachment has 'publish' status set by plugin):
# {
#   "post": {
#     "featuredImage": null  # ❌ Should return the attachment
#   },
#   "mediaItems": {
#     "nodes": []  # ❌ Should return the attachment
#   }
# }

After (Attachments Found - Issue #1999):

# Query as admin
query {
  post(id: "123", idType: DATABASE_ID) {
    featuredImage {
      node {
        id
        databaseId
        status
      }
    }
  }
  mediaItems(where: { id: 456 }) {
    nodes {
      id
      databaseId
      status
    }
  }
}

# Result (when attachment has 'publish' status set by plugin):
# {
#   "post": {
#     "featuredImage": {
#       "node": {
#         "id": "...",
#         "databaseId": 456,
#         "status": "publish"  # ✅ Attachment found
#       }
#     }
#   },
#   "mediaItems": {
#     "nodes": [
#       {
#         "id": "...",
#         "databaseId": 456,
#         "status": "publish"  # ✅ Attachment found
#       }
#     ]
#   }
# }

Additional Context

Preserved Behavior (Issue #3438)

  • Attachments without a parent (post_parent = 0) remain public
  • Attachments with published parents remain public
  • Attachments with "inherit" status and deleted parents are treated as public (aligns with documented reasoning)
  • Admins with proper permissions can still access draft parent attachments

Preserved Behavior (Issue #1999)

  • Attachments with 'inherit' status continue to work as before
  • Privacy checks still apply - attachments attached to draft parents are still private for non-admin users
  • The Model's is_private() method (fixed in Issue Unable to fetch draft media by slug #3438) handles privacy checks after the attachment is found

Implementation Details

Issue #3438:

  • The fix follows the same pattern used for revisions, which also inherit privacy from their parent posts
  • When an attachment has "inherit" status and a valid parent, a temporary Post model instance is created to check the parent's privacy status
  • This ensures proper initialization of the parent's post type object and accurate privacy checking

Issue #1999:

  • Changed PostObjectConnectionResolver to use post_status='any' for all attachment queries
  • This catches attachments with any status ('inherit', 'publish', etc.)
  • The Model's is_private() method (fixed in Issue Unable to fetch draft media by slug #3438) then handles privacy checks based on the attachment's parent and status
  • This fix applies to all attachment queries, including featuredImage connections and direct mediaItems queries

Related Code

Issue #3438:

  • src/Model/Post.php - Modified is_private() method (lines 310-346)
  • tests/wpunit/MediaItemQueriesTest.php - Added testMediaItemWithDraftParentInheritsPrivacy() and testMediaItemWithPublishedParentRemainsPublic() tests

Issue #1999:

  • src/Data/Connection/PostObjectConnectionResolver.php - Modified prepare_query_args() method to use post_status='any' for attachments (line 216)
  • tests/wpunit/MediaItemQueriesTest.php - Added testFeaturedImageBugScenarioWithInheritStatus(), testFeaturedImageBugScenarioWithPublishStatus(), and testMediaItemsQueryWithPublishStatus() tests

Overriding Behavior

If you need to make all media items public (regardless of parent status), you can use the graphql_pre_model_data_is_private filter to override this behavior:

/**
 * Make all media items public, even if attached to draft posts
 * This allows fetching featured images of draft posts by slug
 */
add_filter( 'graphql_pre_model_data_is_private', function( $is_private, $model_name, $data ) {
    // Make all media items public
    if ( 'PostObject' === $model_name && isset( $data->post_type ) && 'attachment' === $data->post_type ) {
        return false; // false = not private (public)
    }
    return $is_private; // Return null to use default logic for other types
}, 10, 3 );

Alternatively, you can use the graphql_data_is_private filter to modify the privacy check after it's been determined:

/**
 * Make all media items public after privacy check
 */
add_filter( 'graphql_data_is_private', function( $is_private, $model_name, $data ) {
    if ( 'PostObject' === $model_name && isset( $data->post_type ) && 'attachment' === $data->post_type ) {
        return false; // Force all attachments to be public
    }
    return $is_private;
}, 10, 3 );

…p-graphql#3438)

Adds test coverage for issue wp-graphql#3438 where media items with "inherit"
status attached to draft parent posts were incorrectly accessible
when queried by DATABASE_ID, but correctly private when queried by SLUG.

The test verifies that media items inherit privacy from their parent
posts when they have "inherit" status, ensuring consistent behavior
across all ID type resolution paths.

This test will fail without the corresponding fix.
…phql#3438)

Media items with "inherit" status attached to draft parent posts were
incorrectly accessible when queried by DATABASE_ID, but correctly private
when queried by SLUG.

Modified Post::is_private() to check parent privacy for attachments with
"inherit" status, ensuring consistent behavior across all ID type
resolution paths while preserving documented behavior for media items
without parents or with published parents.
@coveralls
Copy link

coveralls commented Nov 20, 2025

Coverage Status

coverage: 83.619% (+0.007%) from 83.612%
when pulling 7d9c1f2 on jasonbahl:fix/3438-media-item-visibility-issues
into 9d1a4b3 on wp-graphql:develop.

@jasonbahl jasonbahl changed the title test: Add test for media item privacy inheritance from draft parent (… fix(#3438, #1999): Media item privacy inheritance and attachment status queries Nov 20, 2025
…raphql#1999)

Adds testMediaItemsQueryWithPublishStatus() to verify that mediaItems
queries can find attachments with 'publish' status (set by plugins)
when querying by ID.

This test reproduces the scenario described in issue wp-graphql#1999 where
PostObjectConnectionResolver was hardcoding post_status='inherit' for
attachments, causing queries to miss attachments that plugins had changed
to 'publish' status.

The test will fail without the fix to PostObjectConnectionResolver.php
and pass once post_status='any' is used for attachments.

Refs: wp-graphql#1999 (comment)
…p-graphql#1999)

Changed PostObjectConnectionResolver to use post_status='any' for
attachments instead of hardcoding 'inherit'. This ensures queries can
find attachments regardless of their status, including 'publish' status
set by plugins.

Previously, PostObjectConnectionResolver was hardcoding post_status='inherit'
for all attachment queries, causing queries to miss attachments that
plugins had changed to 'publish' status. This resulted in featuredImage
returning null and mediaItems queries missing attachments even when they
existed.

The Model's is_private() method (fixed in wp-graphql#3438) handles privacy checks
based on the attachment's parent and status, so using 'any' status is
safe and allows proper privacy enforcement.

This fix applies to all attachment queries, including featuredImage
connections and direct mediaItems queries.

Refs: wp-graphql#1999 (comment)
Adds testGraphqlPreModelDataIsPrivateFilterMakesAttachmentsPublic() and
testGraphqlDataIsPrivateFilterMakesAttachmentsPublic() to verify that
the documented filters for overriding media item privacy work correctly.

These tests ensure that users can use the filters documented in the PR
description to make all media items public, even when attached to draft
posts. This helps prevent regressions for users relying on these filters.

The tests verify both filter hooks:
- graphql_pre_model_data_is_private (before privacy check)
- graphql_data_is_private (after privacy check)

Both filters should allow subscribers to access attachments attached to
draft parents, overriding the default privacy inheritance behavior.
@jasonbahl jasonbahl merged commit 7b0957d into wp-graphql:develop Nov 21, 2025
38 checks passed
pull bot pushed a commit to Zezo-Ai/wp-graphql that referenced this pull request Nov 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unable to fetch draft media by slug featuredImage is null even it has image. JWT Authentication Make Readme Better

2 participants