Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
81 changes: 81 additions & 0 deletions src/wp-includes/capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,87 @@ 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` mapping.
*
* @since 7.1.0
*
* @param 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 bool[] Filtered array of the user's capabilities.
*/
function wp_maybe_grant_knowledge_caps( array $allcaps, array $caps, array $args, WP_User $user ): array {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Core has historically avoided scalar/array hints on filter callbacks for graceful coercion see wp_maybe_grant_site_health_caps above. But it is new function although not following the previous patterns maybe we should embrace it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that might make sense.

Aside, the function might need to be marked as private and prefixed with _.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A coding standards change was made some time ago to avoid the _ prefix for private functions so it can just be annotated as such without the underscore.

if ( ! empty( $allcaps['manage_options'] ) ) {
$allcaps['read_knowledge'] = true;
$allcaps['edit_knowledge'] = true;
$allcaps['edit_others_knowledge'] = true;
$allcaps['edit_published_knowledge'] = true;
$allcaps['edit_private_knowledge'] = true;
$allcaps['publish_knowledge'] = true;
$allcaps['delete_knowledge'] = true;
$allcaps['delete_others_knowledge'] = true;
$allcaps['delete_published_knowledge'] = true;
$allcaps['delete_private_knowledge'] = true;
$allcaps['read_private_knowledge'] = true;

return $allcaps;
}

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

/*
* Ambient floor for contributors and above: `read_knowledge` clears the
* post-type read check; `edit_knowledge` 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'] = true;
$allcaps['edit_knowledge'] = true;

if ( ! isset( $args[0], $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 ||
'private' !== $post->post_status

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think contributor-level users can likely trash their own private knowledge, then lose the ability to permanently delete it. Because grant only applies when post_status === 'private', so once the item is in trash, delete_knowledge is no longer granted.

) {
return $allcaps;
}

$allcaps['edit_private_knowledge'] = true;
$allcaps['delete_knowledge'] = true;
$allcaps['delete_private_knowledge'] = true;
$allcaps['read_private_knowledge'] = 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 @@ -773,6 +773,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 @@ -786,6 +787,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( 'save_post_wp_knowledge', '_wp_knowledge_ensure_default_type_term' );
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
142 changes: 142 additions & 0 deletions src/wp-includes/knowledge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?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", "memory", 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.1.0
*/

/**
* Retrieves the registered knowledge types, keyed by slug.
*
* Plugins can register their own types via the {@see 'wp_knowledge_types'} filter.
*
* @since 7.1.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.1.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(
'wp_knowledge_types',
array(
'guideline' => array(
'title' => _x( 'Guideline', 'knowledge type' ),
),
'memory' => array(
'title' => _x( 'Memory', 'knowledge type' ),
),
'note' => array(
'title' => _x( 'Note', 'knowledge type' ),
),
)
);
}

/**
* Assigns the `note` fallback term when a knowledge post is saved without a type.
*
* Hooked to the `save_post_wp_knowledge` action so that every knowledge row has
* at least one `wp_knowledge_type` term. Uses get_the_terms() so the check is
* served by the object term cache.
*
* @since 7.1.0
* @access private
*
* @param int $post_id Saved post ID.
*/
function _wp_knowledge_ensure_default_type_term( int $post_id ): void {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per earlier comment, requires changes elsewhere.

Suggested change
function _wp_knowledge_ensure_default_type_term( int $post_id ): void {
function wp_knowledge_ensure_default_type_term( int $post_id ): void {

if ( wp_is_post_revision( $post_id ) ) {
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 wp_knowledge_types(). Because term names are
* persisted in the database, the translated title is stored in the locale active
* when the term is created.
*
* @since 7.1.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<non-empty-string, mixed> $data
* @phpstan-return array<non-empty-string, mixed>
*/
function _wp_knowledge_maybe_map_term_label( array $data, string $taxonomy ): array {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per earlier comment, requires changes elsewhere.

Suggested change
function _wp_knowledge_maybe_map_term_label( array $data, string $taxonomy ): array {
function wp_knowledge_maybe_map_term_label( array $data, string $taxonomy ): array {

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

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

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

return $data;
}
50 changes: 50 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.1.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,55 @@ function create_initial_post_types() {
)
);

register_post_type(
'wp_knowledge',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not setting delete_with_user, so we use the default and knowledge created by an user is deleted when that user is deleted. I guess that behaviour is correct if the knowledge is private, but it may have unintended consequences for knowledge memory shared between users.

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,
/*
* "Knowledge" is a mass noun, so the singular and plural capability
* bases must differ: with both set to `knowledge`, the generated
* per-post meta caps (`edit_knowledge_item`) would collide with the
* primitive caps (`edit_knowledge`). The `*_knowledge_item` forms are
* never granted directly; `map_meta_cap()` resolves them onto the
* primitives, which `wp_maybe_grant_knowledge_caps()` synthesizes.
*/
'capability_type' => array( 'knowledge_item', 'knowledge' ),
Comment on lines +677 to +685

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/*
* "Knowledge" is a mass noun, so the singular and plural capability
* bases must differ: with both set to `knowledge`, the generated
* per-post meta caps (`edit_knowledge_item`) would collide with the
* primitive caps (`edit_knowledge`). The `*_knowledge_item` forms are
* never granted directly; `map_meta_cap()` resolves them onto the
* primitives, which `wp_maybe_grant_knowledge_caps()` synthesizes.
*/
'capability_type' => array( 'knowledge_item', 'knowledge' ),
'capability_type' => array( 'knowledge_items', 'knowledge_item' ),

...or similar: as a general rule primitives are plural and meta are the singular form of the same. Eg edit_posts allows editing of posts, whereas edit_post allows the editing of a single post.

Requires changes elsewhere.

/*
* `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`-prefixed capability granted by
* `wp_maybe_grant_knowledge_caps()`.
*/
'capabilities' => array(
'read' => 'read_knowledge',
),
'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