Skip to content

Commit 69b1806

Browse files
gzioloclaude
andcommitted
Knowledge: Introduce the wp_knowledge custom post type.
Register wp_knowledge, a private-by-default storage primitive for structured site knowledge, together with the wp_knowledge_type taxonomy. Both are built-in and headless (no admin UI); rows are managed through the REST API. Add WP_REST_Knowledge_Controller at /wp/v2/knowledge: reads require an authenticated user, collections are scoped to rows the current user can read, callers without the publish capability are limited to the private status, and new rows default to private. Revisions use the default controller; autosave endpoints are disabled. Synthesize a graded capability set through the user_has_cap filter: administrators manage all knowledge, while contributors and above create and manage their own private rows. Knowledge types are filterable via wp_knowledge_types(), with note as the save-time fallback. Trac ticket: https://core.trac.wordpress.org/ticket/65476 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1caffb5 commit 69b1806

13 files changed

Lines changed: 2465 additions & 65 deletions

File tree

src/wp-includes/capabilities.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,87 @@ function wp_maybe_grant_site_health_caps( $allcaps, $caps, $args, $user ) {
13651365
return $allcaps;
13661366
}
13671367

1368+
/**
1369+
* Filters the user capabilities to grant the `wp_knowledge` post type capabilities as necessary.
1370+
*
1371+
* The `wp_knowledge` post type uses a `knowledge`-prefixed capability set that is
1372+
* granted dynamically rather than stored on roles. Administrators (users with
1373+
* `manage_options`) receive every knowledge capability. Contributors, authors,
1374+
* and editors (users with `edit_posts`) may list and create knowledge rows and
1375+
* fully manage their own private rows; publishing knowledge and acting on other
1376+
* users' rows is reserved for administrators. Subscribers receive nothing and
1377+
* are stopped at the post-type door by the `read_knowledge` mapping.
1378+
*
1379+
* @since 7.1.0
1380+
*
1381+
* @param bool[] $allcaps An array of all the user's capabilities.
1382+
* @param string[] $caps Required primitive capabilities for the requested capability.
1383+
* @param array $args {
1384+
* Arguments that accompany the requested capability check.
1385+
*
1386+
* @type string $0 Requested capability.
1387+
* @type int $1 Concerned user ID.
1388+
* @type mixed ...$2 Optional second and further parameters, typically object ID.
1389+
* }
1390+
* @param WP_User $user The user object.
1391+
* @return bool[] Filtered array of the user's capabilities.
1392+
*/
1393+
function wp_maybe_grant_knowledge_caps( array $allcaps, array $caps, array $args, WP_User $user ): array {
1394+
if ( ! empty( $allcaps['manage_options'] ) ) {
1395+
$allcaps['read_knowledge'] = true;
1396+
$allcaps['edit_knowledge'] = true;
1397+
$allcaps['edit_others_knowledge'] = true;
1398+
$allcaps['edit_published_knowledge'] = true;
1399+
$allcaps['edit_private_knowledge'] = true;
1400+
$allcaps['publish_knowledge'] = true;
1401+
$allcaps['delete_knowledge'] = true;
1402+
$allcaps['delete_others_knowledge'] = true;
1403+
$allcaps['delete_published_knowledge'] = true;
1404+
$allcaps['delete_private_knowledge'] = true;
1405+
$allcaps['read_private_knowledge'] = true;
1406+
1407+
return $allcaps;
1408+
}
1409+
1410+
if ( empty( $allcaps['edit_posts'] ) ) {
1411+
return $allcaps;
1412+
}
1413+
1414+
/*
1415+
* Ambient floor for contributors and above: `read_knowledge` clears the
1416+
* post-type read check; `edit_knowledge` clears the create and ownership
1417+
* checks that do not pass a post ID. Per-post primitives are granted only
1418+
* in the per-post branch below.
1419+
*/
1420+
$allcaps['read_knowledge'] = true;
1421+
$allcaps['edit_knowledge'] = true;
1422+
1423+
if ( ! isset( $args[0], $args[2] ) ) {
1424+
return $allcaps;
1425+
}
1426+
1427+
if ( ! in_array( $args[0], array( 'edit_post', 'delete_post', 'read_post' ), true ) ) {
1428+
return $allcaps;
1429+
}
1430+
1431+
$post = get_post( $args[2] );
1432+
if (
1433+
! $post instanceof WP_Post ||
1434+
'wp_knowledge' !== $post->post_type ||
1435+
(int) $post->post_author !== (int) $user->ID ||
1436+
'private' !== $post->post_status
1437+
) {
1438+
return $allcaps;
1439+
}
1440+
1441+
$allcaps['edit_private_knowledge'] = true;
1442+
$allcaps['delete_knowledge'] = true;
1443+
$allcaps['delete_private_knowledge'] = true;
1444+
$allcaps['read_private_knowledge'] = true;
1445+
1446+
return $allcaps;
1447+
}
1448+
13681449
return;
13691450

13701451
// Dummy gettext calls to get strings in the catalog.

src/wp-includes/default-filters.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,7 @@
773773
add_filter( 'user_has_cap', 'wp_maybe_grant_install_languages_cap', 1 );
774774
add_filter( 'user_has_cap', 'wp_maybe_grant_resume_extensions_caps', 1 );
775775
add_filter( 'user_has_cap', 'wp_maybe_grant_site_health_caps', 1, 4 );
776+
add_filter( 'user_has_cap', 'wp_maybe_grant_knowledge_caps', 1, 4 );
776777

777778
// Block templates post type and rendering.
778779
add_filter( 'render_block_context', '_block_template_render_without_post_block_context' );
@@ -786,6 +787,10 @@
786787
// wp_navigation post type.
787788
add_filter( 'rest_wp_navigation_item_schema', array( 'WP_Navigation_Fallback', 'update_wp_navigation_post_schema' ) );
788789

790+
// wp_knowledge post type.
791+
add_action( 'save_post_wp_knowledge', '_wp_knowledge_ensure_default_type_term' );
792+
add_filter( 'wp_insert_term_data', '_wp_knowledge_maybe_map_term_label', 10, 2 );
793+
789794
// Fluid typography.
790795
add_filter( 'render_block', 'wp_render_typography_support', 10, 2 );
791796

src/wp-includes/knowledge.php

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
/**
3+
* Knowledge API: Public functions for the `wp_knowledge` post type.
4+
*
5+
* The Knowledge post type is a private-by-default storage primitive. Individual
6+
* rows are classified by one or more terms in the `wp_knowledge_type` taxonomy
7+
* (for example "guideline", "memory", or "note"). This file holds the type
8+
* registry, the default-term fallback applied on save, and the helper that
9+
* gives lazily created type terms a human-readable label.
10+
*
11+
* @package WordPress
12+
* @subpackage Knowledge
13+
* @since 7.1.0
14+
*/
15+
16+
/**
17+
* Retrieves the registered knowledge types, keyed by slug.
18+
*
19+
* Plugins can register their own types via the {@see 'wp_knowledge_types'} filter.
20+
*
21+
* @since 7.1.0
22+
*
23+
* @return array {
24+
* Slug-keyed map of knowledge types.
25+
*
26+
* @type array ...$0 {
27+
* Data for a single knowledge type.
28+
*
29+
* @type string $title The human-readable label for the type.
30+
* }
31+
* }
32+
* @phpstan-return array<string, array{title: string}>
33+
*/
34+
function wp_knowledge_types(): array {
35+
/**
36+
* Filters the knowledge types available on this site.
37+
*
38+
* @since 7.1.0
39+
*
40+
* @param array $types {
41+
* Slug-keyed map of knowledge types.
42+
*
43+
* @type array ...$0 {
44+
* Data for a single knowledge type.
45+
*
46+
* @type string $title The human-readable label for the type.
47+
* }
48+
* }
49+
* @phpstan-param array<string, array{title: string}> $types
50+
*/
51+
return apply_filters(
52+
'wp_knowledge_types',
53+
array(
54+
'guideline' => array(
55+
'title' => _x( 'Guideline', 'knowledge type' ),
56+
),
57+
'memory' => array(
58+
'title' => _x( 'Memory', 'knowledge type' ),
59+
),
60+
'note' => array(
61+
'title' => _x( 'Note', 'knowledge type' ),
62+
),
63+
)
64+
);
65+
}
66+
67+
/**
68+
* Assigns the `note` fallback term when a knowledge post is saved without a type.
69+
*
70+
* Hooked to the `save_post_wp_knowledge` action so that every knowledge row has
71+
* at least one `wp_knowledge_type` term. Uses get_the_terms() so the check is
72+
* served by the object term cache.
73+
*
74+
* @since 7.1.0
75+
* @access private
76+
*
77+
* @param int $post_id Saved post ID.
78+
*/
79+
function _wp_knowledge_ensure_default_type_term( int $post_id ): void {
80+
if ( wp_is_post_revision( $post_id ) ) {
81+
return;
82+
}
83+
84+
$terms = get_the_terms( $post_id, 'wp_knowledge_type' );
85+
if ( is_wp_error( $terms ) || ! empty( $terms ) ) {
86+
return;
87+
}
88+
89+
/*
90+
* Resolve to a term ID up front, creating the term on first use:
91+
* wp_set_object_terms() interprets strings as names for hierarchical
92+
* taxonomies, not slugs.
93+
*/
94+
$term = term_exists( 'note', 'wp_knowledge_type' );
95+
if ( ! $term ) {
96+
$term = wp_insert_term( 'note', 'wp_knowledge_type' );
97+
if ( is_wp_error( $term ) ) {
98+
return;
99+
}
100+
}
101+
102+
wp_set_object_terms( $post_id, (int) $term['term_id'], 'wp_knowledge_type' );
103+
}
104+
105+
/**
106+
* Swaps a raw knowledge-type slug for its registered label on term creation.
107+
*
108+
* Hooked to the `wp_insert_term_data` filter. When wp_set_object_terms() is
109+
* called with a slug that does not yet exist, wp_insert_term() fires and this
110+
* filter runs after WordPress has computed both `name` and `slug`. A `name`
111+
* equal to `slug` indicates the term was created from a raw slug (for example by
112+
* wp_set_object_terms()) rather than from a user-provided label, so the label is
113+
* replaced with the title from wp_knowledge_types(). Because term names are
114+
* persisted in the database, the translated title is stored in the locale active
115+
* when the term is created.
116+
*
117+
* @since 7.1.0
118+
* @access private
119+
*
120+
* @param array $data Term data to be inserted (keyed by column name).
121+
* @param string $taxonomy Taxonomy slug.
122+
* @return array Possibly modified term data.
123+
*
124+
* @phpstan-param array<string, mixed> $data
125+
* @phpstan-return array<string, mixed>
126+
*/
127+
function _wp_knowledge_maybe_map_term_label( array $data, string $taxonomy ): array {
128+
if ( 'wp_knowledge_type' !== $taxonomy ) {
129+
return $data;
130+
}
131+
132+
if ( $data['name'] !== $data['slug'] ) {
133+
return $data;
134+
}
135+
136+
$types = wp_knowledge_types();
137+
if ( isset( $types[ $data['slug'] ] ) ) {
138+
$data['name'] = $types[ $data['slug'] ]['title'];
139+
}
140+
141+
return $data;
142+
}

src/wp-includes/post.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* See {@see 'init'}.
1717
*
1818
* @since 2.9.0
19+
* @since 7.1.0 Added the `wp_knowledge` post type.
1920
*/
2021
function create_initial_post_types() {
2122
WP_Post_Type::reset_default_labels();
@@ -657,6 +658,55 @@ function create_initial_post_types() {
657658
)
658659
);
659660

661+
register_post_type(
662+
'wp_knowledge',
663+
array(
664+
'labels' => array(
665+
'name' => _x( 'Knowledge', 'post type general name' ),
666+
'singular_name' => _x( 'Knowledge Item', 'post type singular name' ),
667+
),
668+
'public' => false,
669+
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
670+
'hierarchical' => false,
671+
/*
672+
* Knowledge rows have no native post-type screens; they are managed
673+
* through the REST API and consuming features, not the wp-admin UI.
674+
*/
675+
'show_ui' => false,
676+
'map_meta_cap' => true,
677+
/*
678+
* "Knowledge" is a mass noun, so the singular and plural capability
679+
* bases must differ: with both set to `knowledge`, the generated
680+
* per-post meta caps (`edit_knowledge_item`) would collide with the
681+
* primitive caps (`edit_knowledge`). The `*_knowledge_item` forms are
682+
* never granted directly; `map_meta_cap()` resolves them onto the
683+
* primitives, which `wp_maybe_grant_knowledge_caps()` synthesizes.
684+
*/
685+
'capability_type' => array( 'knowledge_item', 'knowledge' ),
686+
/*
687+
* `read` is remapped so that subscribers (who hold the base `read`
688+
* capability) are stopped at the post-type door. Every other
689+
* primitive defaults to a `knowledge`-prefixed capability granted by
690+
* `wp_maybe_grant_knowledge_caps()`.
691+
*/
692+
'capabilities' => array(
693+
'read' => 'read_knowledge',
694+
),
695+
'query_var' => false,
696+
'rewrite' => false,
697+
'show_in_rest' => true,
698+
'rest_base' => 'knowledge',
699+
'rest_controller_class' => 'WP_REST_Knowledge_Controller',
700+
'supports' => array( 'title', 'editor', 'excerpt', 'author', 'revisions' ),
701+
)
702+
);
703+
/*
704+
* Disable autosave endpoints for knowledge. 'editor' support implies
705+
* 'autosave', but knowledge is headless storage with no editor session, so
706+
* the autosave REST routes have no consumer. Revision history is retained.
707+
*/
708+
remove_post_type_support( 'wp_knowledge', 'autosave' );
709+
660710
register_post_status(
661711
'publish',
662712
array(

0 commit comments

Comments
 (0)