Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7508b55
Knowledge: Introduce the wp_knowledge custom post type.
gziolo Jun 17, 2026
84b21a4
Knowledge: Address PR feedback on naming and capability filter.
gziolo Jun 18, 2026
1a05ea2
Knowledge: Use plural/singular capability bases per convention.
gziolo Jun 18, 2026
e7d4f1c
Knowledge: Strengthen REST controller permission test coverage.
gziolo Jun 18, 2026
5c459e8
Knowledge: Keep author control of their own trashed rows.
gziolo Jun 18, 2026
a65e9d8
Knowledge: Align the collection read error code with core controllers.
gziolo Jun 18, 2026
a65c11c
Knowledge: Drop the no-op show_admin_column from wp_knowledge_type.
gziolo Jun 18, 2026
a155832
Fix type issues with wp_maybe_grant_knowledge_caps()
westonruter Jun 19, 2026
3733630
Fix typing with wp_knowledge_maybe_map_term_label()
westonruter Jun 19, 2026
3180f8e
Fix typing on prepare_items_query()
westonruter Jun 19, 2026
2c6665b
Add types to wp_maybe_grant_knowledge_caps()
westonruter Jun 19, 2026
c021f5a
Guard against post type not being registered in get_items_permissions…
westonruter Jun 19, 2026
9781410
Knowledge: Resolve type-term labels in the site locale.
gziolo Jun 30, 2026
a924b44
Fix param docs indentation
westonruter Jul 9, 2026
7bcf1e4
Improve docblock formatting
westonruter Jul 9, 2026
52235ad
Fix most phpstan issues in tests and use native type hints
westonruter Jul 9, 2026
b8aa419
Fix phpstan errors in tests
westonruter Jul 9, 2026
2b6582c
Knowledge: Enforce the default type term after all REST term writes.
gziolo Jul 13, 2026
8d32e60
Knowledge: Move @since tags from 7.1.0 to 7.2.0.
gziolo Jul 13, 2026
c6023eb
Knowledge: Drop the `memory` default type.
gziolo Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions src/wp-includes/capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,107 @@ function wp_maybe_grant_site_health_caps( $allcaps, $caps, $args, $user ) {
return $allcaps;
}

/**
* Filters the user capabilities to grant the `wp_knowledge` post type capabilities as necessary.
*
* The `wp_knowledge` post type uses a `knowledge`-prefixed capability set that is
* granted dynamically rather than stored on roles. Administrators (users with
* `manage_options`) receive every knowledge capability. Contributors, authors,
* and editors (users with `edit_posts`) may list and create knowledge rows and
* fully manage their own private rows. Publishing knowledge and acting on other
* users' rows is reserved for administrators. Subscribers receive nothing and
* are stopped at the post-type door by the `read_knowledge_items` mapping.
*
* @since 7.2.0
*
* @param array<string, bool> $allcaps An array of all the user's capabilities.
* @param string[] $caps Required primitive capabilities for the requested capability.
* @param array $args {
* Arguments that accompany the requested capability check.
*
* @type string $0 Requested capability.
* @type int $1 Concerned user ID.
* @type mixed ...$2 Optional second and further parameters, typically object ID.
* }
* @param WP_User $user The user object.
* @return array<string, bool> Filtered array of the user's capabilities.
* @phpstan-param array{ 0: string, 1: positive-int, 2: mixed, ... } $args
*/
function wp_maybe_grant_knowledge_caps( $allcaps, array $caps, array $args, WP_User $user ): array {
if ( ! is_array( $allcaps ) ) {
$allcaps = array();
}

if ( ! empty( $allcaps['manage_options'] ) ) {
$allcaps['read_knowledge_items'] = true;
$allcaps['edit_knowledge_items'] = true;
$allcaps['edit_others_knowledge_items'] = true;
$allcaps['edit_published_knowledge_items'] = true;
$allcaps['edit_private_knowledge_items'] = true;
$allcaps['publish_knowledge_items'] = true;
$allcaps['delete_knowledge_items'] = true;
$allcaps['delete_others_knowledge_items'] = true;
$allcaps['delete_published_knowledge_items'] = true;
$allcaps['delete_private_knowledge_items'] = true;
$allcaps['read_private_knowledge_items'] = true;

return $allcaps;
}

if ( empty( $allcaps['edit_posts'] ) ) {
return $allcaps;
}

/*
* Ambient floor for contributors and above: `read_knowledge_items` clears the
* post-type read check; `edit_knowledge_items` clears the create and ownership
* checks that do not pass a post ID. Per-post primitives are granted only
* in the per-post branch below.
*/
$allcaps['read_knowledge_items'] = true;
$allcaps['edit_knowledge_items'] = true;

if ( ! isset( $args[0], $args[2] ) || ! is_int( $args[2] ) ) {
return $allcaps;
}

if ( ! in_array( $args[0], array( 'edit_post', 'delete_post', 'read_post' ), true ) ) {
return $allcaps;
}

$post = get_post( $args[2] );
if (
! $post instanceof WP_Post ||
'wp_knowledge' !== $post->post_type ||
(int) $post->post_author !== (int) $user->ID
) {
return $allcaps;
}

/*
* A trashed row keeps its pre-trash status in `_wp_trash_meta_status`.
* Resolve that effective status so the author keeps the ability to restore
* or permanently delete their own row once it is in the trash. A row trashed
* from a non-private status (only reachable for administrators) still falls
* outside the grant.
*/
$status = $post->post_status;
if ( 'trash' === $status ) {
$status = get_post_meta( $post->ID, '_wp_trash_meta_status', true );
}

if ( 'private' !== $status ) {
return $allcaps;
}

$allcaps['edit_private_knowledge_items'] = true;
$allcaps['delete_knowledge_items'] = true;
$allcaps['delete_private_knowledge_items'] = true;
$allcaps['read_private_knowledge_items'] = true;

return $allcaps;
}

return;

// Dummy gettext calls to get strings in the catalog.
Expand Down
5 changes: 5 additions & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@
add_filter( 'user_has_cap', 'wp_maybe_grant_install_languages_cap', 1 );
add_filter( 'user_has_cap', 'wp_maybe_grant_resume_extensions_caps', 1 );
add_filter( 'user_has_cap', 'wp_maybe_grant_site_health_caps', 1, 4 );
add_filter( 'user_has_cap', 'wp_maybe_grant_knowledge_caps', 1, 4 );

// Block templates post type and rendering.
add_filter( 'render_block_context', '_block_template_render_without_post_block_context' );
Expand All @@ -788,6 +789,10 @@
// wp_navigation post type.
add_filter( 'rest_wp_navigation_item_schema', array( 'WP_Navigation_Fallback', 'update_wp_navigation_post_schema' ) );

// wp_knowledge post type.
add_action( 'wp_after_insert_post', 'wp_knowledge_ensure_default_type_term', 10, 2 );
add_filter( 'wp_insert_term_data', 'wp_knowledge_maybe_map_term_label', 10, 2 );

// Fluid typography.
add_filter( 'render_block', 'wp_render_typography_support', 10, 2 );

Expand Down
155 changes: 155 additions & 0 deletions src/wp-includes/knowledge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?php
/**
* Knowledge API: Public functions for the `wp_knowledge` post type.
*
* The Knowledge post type is a private-by-default storage primitive. Individual
* rows are classified by one or more terms in the `wp_knowledge_type` taxonomy
* (for example "guideline" or "note"). This file holds the type
* registry, the default-term fallback applied on save, and the helper that
* gives lazily created type terms a human-readable label.
*
* @package WordPress
* @subpackage Knowledge
* @since 7.2.0
*/

/**
* Retrieves the registered knowledge types, keyed by slug.
*
* Plugins can register their own types via the {@see 'wp_knowledge_types'} filter.
*
* @since 7.2.0
*
* @return array {
* Slug-keyed map of knowledge types.
*
* @type array ...$0 {
* Data for a single knowledge type.
*
* @type string $title The human-readable label for the type.
* }
* }
* @phpstan-return array<non-empty-string, array{title: non-empty-string}>
*/
function wp_knowledge_types(): array {
/**
* Filters the knowledge types available on this site.
*
* @since 7.2.0
*
* @param array $types {
* Slug-keyed map of knowledge types.
*
* @type array ...$0 {
* Data for a single knowledge type.
*
* @type string $title The human-readable label for the type.
* }
* }
* @phpstan-param array<non-empty-string, array{title: non-empty-string}> $types
*/
return apply_filters(
Comment thread
gziolo marked this conversation as resolved.
'wp_knowledge_types',
array(
'guideline' => array(
'title' => _x( 'Guideline', 'knowledge type' ),
),
'note' => array(
'title' => _x( 'Note', 'knowledge type' ),
),
)
);
}

/**
* Ensures every knowledge row carries at least one type term.
*
* Knowledge rows are classified by their `wp_knowledge_type` terms, so a row
* saved without one would belong to no type at all. This assigns the `note`
* fallback in that case, keeping every row classified.
*
* Hooked to `wp_after_insert_post` so it runs after the row's terms are saved,
* when the final set of terms is known.
*
* @since 7.2.0
* @access private
*
* @param int $post_id Saved post ID.
* @param WP_Post $post Saved post object.
*/
function wp_knowledge_ensure_default_type_term( int $post_id, WP_Post $post ): void {
if ( 'wp_knowledge' !== $post->post_type ) {
return;
}

$terms = get_the_terms( $post_id, 'wp_knowledge_type' );
if ( is_wp_error( $terms ) || ! empty( $terms ) ) {
return;
}

/*
* Resolve to a term ID up front, creating the term on first use:
* wp_set_object_terms() interprets strings as names for hierarchical
* taxonomies, not slugs.
*/
$term = term_exists( 'note', 'wp_knowledge_type' );
if ( ! $term ) {
$term = wp_insert_term( 'note', 'wp_knowledge_type' );
if ( is_wp_error( $term ) ) {
return;
}
}

wp_set_object_terms( $post_id, (int) $term['term_id'], 'wp_knowledge_type' );
}

/**
* Swaps a raw knowledge-type slug for its registered label on term creation.
*
* Hooked to the `wp_insert_term_data` filter. When `wp_set_object_terms()` is
* called with a slug that does not yet exist, wp_insert_term() fires and this
* filter runs after WordPress has computed both `name` and `slug`. A `name`
* equal to `slug` indicates the term was created from a raw slug (for example by
* `wp_set_object_terms()`) rather than from a user-provided label, so the label is
* replaced with the title from {@see wp_knowledge_types()}.
*
* The name is written once and shared by every user, so it is resolved in the
* site locale, not the locale of the request that happens to create the term.
*
* @since 7.2.0
* @access private
*
* @param array $data Term data to be inserted (keyed by column name).
* @param string $taxonomy Taxonomy slug.
* @return array Possibly modified term data.
*
* @phpstan-param array{ name: string, slug: string } $data
* @phpstan-return array{ name: string, slug: string }
*/
function wp_knowledge_maybe_map_term_label( $data, string $taxonomy ): array {
if ( ! is_array( $data ) ) {
$data = array_fill_keys( array( 'name', 'slug' ), '' );
}

if ( 'wp_knowledge_type' !== $taxonomy ) {
return $data;
}

if ( $data['name'] !== $data['slug'] ) {
return $data;
}

// Type titles may be translatable, so resolve them under the site locale.
$switched_locale = switch_to_locale( get_locale() );

$types = wp_knowledge_types();
if ( isset( $types[ $data['slug'] ] ) ) {
$data['name'] = $types[ $data['slug'] ]['title'];
}

if ( $switched_locale ) {
restore_previous_locale();
}

return $data;
}
42 changes: 42 additions & 0 deletions src/wp-includes/post.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* See {@see 'init'}.
*
* @since 2.9.0
* @since 7.2.0 Added the `wp_knowledge` post type.
*/
function create_initial_post_types() {
WP_Post_Type::reset_default_labels();
Expand Down Expand Up @@ -657,6 +658,47 @@ function create_initial_post_types() {
)
);

register_post_type(
'wp_knowledge',
Comment thread
gziolo marked this conversation as resolved.
array(
'labels' => array(
'name' => _x( 'Knowledge', 'post type general name' ),
'singular_name' => _x( 'Knowledge Item', 'post type singular name' ),
),
'public' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'hierarchical' => false,
/*
* Knowledge rows have no native post-type screens. They are managed
* through the REST API and consuming features, not the wp-admin UI.
*/
'show_ui' => false,
'map_meta_cap' => true,
'capability_type' => array( 'knowledge_item', 'knowledge_items' ),
/*
* `read` is remapped so that subscribers (who hold the base `read`
* capability) are stopped at the post-type door. Every other
* primitive defaults to a `knowledge_items`-suffixed capability granted
* by `wp_maybe_grant_knowledge_caps()`.
*/
'capabilities' => array(
'read' => 'read_knowledge_items',
),
'query_var' => false,
'rewrite' => false,
'show_in_rest' => true,
'rest_base' => 'knowledge',
Comment thread
jorgefilipecosta marked this conversation as resolved.
'rest_controller_class' => 'WP_REST_Knowledge_Controller',
'supports' => array( 'title', 'editor', 'excerpt', 'author', 'revisions' ),
)
);
/*
* Disable autosave endpoints for knowledge. 'editor' support implies
* 'autosave', but knowledge is headless storage with no editor session, so
* the autosave REST routes have no consumer. Revision history is retained.
*/
remove_post_type_support( 'wp_knowledge', 'autosave' );

register_post_status(
'publish',
array(
Expand Down
Loading
Loading