# gutenberg/23.6.0/lib/experimental/knowledge/knowledge.php

Gutenberg, version 23.6.0. 458 lines.

- Page: https://pluginprobe.com/plugins/gutenberg/23.6.0/code/lib/experimental/knowledge/knowledge.php
- Raw: https://pluginprobe.com/plugins/gutenberg/23.6.0/raw/lib/experimental/knowledge/knowledge.php
- Modified: 2026-07-22T11:19:40+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/gutenberg/23.6.0/code/lib/experimental/knowledge/knowledge.php#L10-L20`.

```php
<?php
/**
 * Knowledge public API.
 *
 * @package gutenberg
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! function_exists( 'wp_knowledge_types' ) ) {
	/**
	 * Returns the registered knowledge types keyed by slug.
	 *
	 * Plugins can register their own types via the `wp_knowledge_types` filter.
	 *
	 * @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<string, array{title: string}>
	 */
	function wp_knowledge_types(): array {
		/**
		 * Filters the knowledge types available on this site.
		 *
		 * @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<string, array{title: string}> $types
		 */
		return apply_filters(
			'wp_knowledge_types',
			array(
				'guideline' => array(
					'title' => _x( 'Guideline', 'knowledge type', 'gutenberg' ),
				),
				'memory'    => array(
					'title' => _x( 'Memory', 'knowledge type', 'gutenberg' ),
				),
				'note'      => array(
					'title' => _x( 'Note', 'knowledge type', 'gutenberg' ),
				),
			)
		);
	}
}

if ( ! function_exists( 'wp_guideline_scopes' ) ) {
	/**
	 * Returns the registered guideline scopes keyed by slug.
	 *
	 * Scopes are the sections shown on the Settings → Guidelines page. Each
	 * scope is backed by at most one `guideline`-typed `wp_knowledge` row whose
	 * slug is `guideline-{scope}`. Plugins can register their own scopes via the
	 * `wp_guideline_scopes` filter and the Settings page grows a section
	 * automatically. The registry carries identity and presentation only; rows
	 * are created on first save.
	 *
	 * The `blocks` scope is the one exception: it has no single `guideline-blocks`
	 * row. Its section lists per-block guidelines stored as `guideline-block-*`
	 * rows. Removing it from this registry (via the filter) hides that section on
	 * the Settings page.
	 *
	 * @return array {
	 *     Slug-keyed map of guideline scopes.
	 *
	 *     @type array ...$0 {
	 *         Data for a single scope.
	 *
	 *         @type string $title       Human-readable section title.
	 *         @type string $description Human-readable section description.
	 *         @type int    $order       Sort order on the Settings page.
	 *     }
	 * }
	 * @phpstan-return array<string, array{title: string, description: string, order: int}>
	 */
	function wp_guideline_scopes(): array {
		/**
		 * Filters the guideline scopes available on this site.
		 *
		 * @param array $scopes Slug-keyed map of guideline scopes.
		 */
		return apply_filters(
			'wp_guideline_scopes',
			array(
				'site'       => array(
					'title'       => __( 'Site', 'gutenberg' ),
					'description' => __( "Describe your site's purpose, goals, and primary audience.", 'gutenberg' ),
					'order'       => 10,
				),
				'copy'       => array(
					'title'       => __( 'Copy', 'gutenberg' ),
					'description' => __( 'Set your writing standards for tone, voice, style, and formatting.', 'gutenberg' ),
					'order'       => 20,
				),
				'images'     => array(
					'title'       => __( 'Images', 'gutenberg' ),
					'description' => __( 'Outline your style, dimensions, formats, mood and aesthetic preferences.', 'gutenberg' ),
					'order'       => 30,
				),
				'blocks'     => array(
					'title'       => __( 'Blocks', 'gutenberg' ),
					'description' => __( 'Create tailored guidelines for specific block types.', 'gutenberg' ),
					'order'       => 40,
				),
				'additional' => array(
					'title'       => __( 'Additional', 'gutenberg' ),
					'description' => __( 'Add additional guidelines.', 'gutenberg' ),
					'order'       => 50,
				),
			)
		);
	}
}

if ( ! function_exists( 'wp_guideline_max_length' ) ) {
	/**
	 * Returns the maximum length, in characters, of a guideline row's content.
	 *
	 * @return int Maximum number of characters allowed in guideline content.
	 */
	function wp_guideline_max_length(): int {
		/**
		 * Filters the maximum length, in characters, of a guideline row's content.
		 *
		 * @param int $max_length Maximum number of characters. Default 5000.
		 */
		return (int) apply_filters( 'wp_guideline_max_length', 5000 );
	}
}

if ( ! function_exists( 'wp_knowledge_get_or_create_type_term' ) ) {
	/**
	 * Resolve a `wp_knowledge_type` term by slug, creating it lazily.
	 *
	 * Created term names are written once in the site locale (via
	 * `wp_knowledge_maybe_map_term_label`) so they don't vary with whoever
	 * triggered creation.
	 *
	 * @access private
	 *
	 * @param string $slug Term slug.
	 * @return int|null Term ID, or null on failure.
	 */
	function wp_knowledge_get_or_create_type_term( string $slug ): ?int {
		$term = term_exists( $slug, 'wp_knowledge_type' );
		if ( $term ) {
			return (int) $term['term_id'];
		}

		$switched = switch_to_locale( get_locale() );
		$term     = wp_insert_term( $slug, 'wp_knowledge_type' );
		if ( $switched ) {
			restore_previous_locale();
		}

		if ( is_wp_error( $term ) ) {
			return null;
		}

		return (int) $term['term_id'];
	}
}

if ( ! function_exists( 'wp_knowledge_ensure_default_type_term' ) ) {
	/**
	 * Hook callback for the `save_post_wp_knowledge` action that assigns the
	 * knowledge type term.
	 *
	 * Rows whose slug begins with `guideline-` are forced onto the `guideline`
	 * type (the reservation rule: the prefix is reserved for guideline-typed
	 * rows). Any other row without a type term falls back to `note`.
	 *
	 * @access private
	 *
	 * @param int $post_id Saved post ID.
	 */
	function wp_knowledge_ensure_default_type_term( int $post_id ): void {
		if ( wp_is_post_revision( $post_id ) ) {
			return;
		}

		$post = get_post( $post_id );
		if ( ! $post instanceof WP_Post ) {
			return;
		}

		if ( str_starts_with( $post->post_name, 'guideline-' ) ) {
			$term_id = wp_knowledge_get_or_create_type_term( 'guideline' );
			if ( null !== $term_id ) {
				wp_set_object_terms( $post_id, $term_id, 'wp_knowledge_type' );
			}
			return;
		}

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

		$term_id = wp_knowledge_get_or_create_type_term( 'note' );
		if ( null !== $term_id ) {
			wp_set_object_terms( $post_id, $term_id, 'wp_knowledge_type' );
		}
	}
}

if ( ! function_exists( 'wp_maybe_grant_knowledge_caps' ) ) {
	/**
	 * 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.
	 *
	 * @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.
	 * @param WP_User  $user    The user object.
	 * @return bool[] Filtered array of the user's capabilities.
	 */
	function wp_maybe_grant_knowledge_caps( $allcaps, $caps, $args, $user ) {
		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 Contributor+: `read_knowledge_items` clears the
		// post-type read check; `edit_knowledge_items` clears the create and
		// ownership checks that don't 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] ) ) {
			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;
	}
}

if ( ! function_exists( 'wp_knowledge_maybe_map_term_label' ) ) {
	/**
	 * Hook callback for the `wp_insert_term_data` filter that swaps a
	 * raw knowledge-type slug for its human-readable label when WordPress
	 * is about to lazily create the term.
	 *
	 * When `wp_set_object_terms()` is called with a slug that doesn't yet
	 * exist, `wp_insert_term()` fires and the filter runs after WP has
	 * computed both `name` and `slug`. A `name` equal to `slug` indicates
	 * the term was created from a raw slug (e.g. by `wp_set_object_terms()`)
	 * rather than from a user-provided label, so the label is replaced with
	 * the title from `wp_knowledge_types()`.
	 *
	 * @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.
	 */
	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;
	}
}

if ( ! function_exists( 'wp_guideline_scope_from_slug' ) ) {
	/**
	 * Resolve the scope that owns a guideline row slug.
	 *
	 * Returns the scope key for a `guideline-{scope}` slug that matches a
	 * registered scope. A registered scope key always wins, so a scope keyed
	 * like `block-foo` resolves to itself rather than the blocks scope. Any
	 * other `guideline-block-*` slug is a per-block row that belongs to the
	 * `blocks` scope while it is registered. Returns null for the bare
	 * `guideline-block-` slug and for any unknown scope.
	 *
	 * @access private
	 *
	 * @param string $slug Post slug.
	 * @return string|null Scope key, or null if the slug is not a registered scope.
	 */
	function wp_guideline_scope_from_slug( string $slug ): ?string {
		if ( ! str_starts_with( $slug, 'guideline-' ) ) {
			return null;
		}

		$scopes = wp_guideline_scopes();
		$scope  = substr( $slug, strlen( 'guideline-' ) );

		// A slug that matches a registered scope key is that scope. Checking this
		// first lets a scope keyed like `block-foo` win over the per-block
		// namespace below, instead of being swallowed by the blocks scope.
		if ( isset( $scopes[ $scope ] ) ) {
			return $scope;
		}

		// Otherwise a `guideline-block-<name>` row is a per-block row that belongs
		// to the blocks scope while it is registered. A real block name never
		// equals a registered scope key, so the check above stays safe.
		if ( str_starts_with( $slug, 'guideline-block-' ) && strlen( $slug ) > strlen( 'guideline-block-' ) ) {
			return isset( $scopes['blocks'] ) ? 'blocks' : null;
		}

		return null;
	}
}

if ( ! function_exists( 'wp_knowledge_guard_guideline_row' ) ) {
	/**
	 * Hook callback for `rest_pre_insert_wp_knowledge` that sanitizes and
	 * normalizes guideline rows on the REST insert path.
	 *
	 * Only rows whose slug maps to a registered guideline scope are shaped (see
	 * `wp_guideline_scope_from_slug()`); any other row is left untouched. For a
	 * recognized guideline row this callback:
	 * - Sanitizes `post_content` to plain text capped at the guideline length.
	 * - Re-stamps the title of single-row registry scopes from
	 *   `wp_guideline_scopes()` in the site locale. Per-block rows (the multi-row
	 *   `blocks` scope) keep the client-provided canonical block name.
	 *
	 * Slug uniqueness is intentionally left to WordPress: the published row keeps
	 * its exact slug because the first save has no conflict and later saves reuse
	 * that row by ID, while any other row with the same desired slug is suffixed
	 * (`guideline-copy-2`) by `wp_unique_post_slug()`. The Settings page reads
	 * only the published row by its exact slug, so suffixed rows are ignored. The
	 * client save flow reclaims an existing same-slug row instead of creating a
	 * duplicate (see routes/guidelines/data.ts).
	 *
	 * @access private
	 *
	 * @param stdClass        $prepared_post Prepared post object.
	 * @param WP_REST_Request $request       Request object.
	 * @return stdClass Prepared post.
	 */
	function wp_knowledge_guard_guideline_row( $prepared_post, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		$slug = '';
		if ( ! empty( $prepared_post->post_name ) ) {
			$slug = $prepared_post->post_name;
		} elseif ( ! empty( $prepared_post->ID ) ) {
			$existing = get_post( $prepared_post->ID );
			if ( $existing instanceof WP_Post ) {
				$slug = $existing->post_name;
			}
		}

		// Only shape rows whose slug maps to a registered guideline scope.
		$scope = wp_guideline_scope_from_slug( (string) $slug );
		if ( null === $scope ) {
			return $prepared_post;
		}

		// Sanitize content: plain text, capped at the guideline length.
		if ( isset( $prepared_post->post_content ) ) {
			$content = sanitize_textarea_field( $prepared_post->post_content );
			$max     = wp_guideline_max_length();
			if ( mb_strlen( $content, 'UTF-8' ) > $max ) {
				$content = mb_substr( $content, 0, $max, 'UTF-8' );
			}
			$prepared_post->post_content = $content;
		}

		// Re-stamp single-row registry scope titles in the site locale. The
		// blocks scope is multi-row, so its per-block rows keep the
		// client-provided canonical block name.
		if ( 'blocks' !== $scope ) {
			$switched = switch_to_locale( get_locale() );
			$scopes   = wp_guideline_scopes();
			if ( $switched ) {
				restore_previous_locale();
			}
			if ( isset( $scopes[ $scope ]['title'] ) ) {
				$prepared_post->post_title = $scopes[ $scope ]['title'];
			}
		}

		return $prepared_post;
	}
}

```
