Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
95.62% |
153 / 160 |
|
55.56% |
5 / 9 |
CRAP | n/a |
0 / 0 |
|
| wp_knowledge_types | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
| wp_guideline_scopes | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
1 | |||
| wp_guideline_max_length | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| wp_knowledge_get_or_create_type_term | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
6.03 | |||
| wp_knowledge_ensure_default_type_term | |
87.50% |
14 / 16 |
|
0.00% |
0 / 1 |
8.12 | |||
| wp_maybe_grant_knowledge_caps | |
94.44% |
34 / 36 |
|
0.00% |
0 / 1 |
11.02 | |||
| wp_knowledge_maybe_map_term_label | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
| wp_guideline_scope_from_slug | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
| wp_knowledge_guard_guideline_row | |
95.83% |
23 / 24 |
|
0.00% |
0 / 1 |
10 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Knowledge public API. |
| 4 | * |
| 5 | * @package automattic/jetpack-knowledge |
| 6 | */ |
| 7 | |
| 8 | if ( ! defined( 'ABSPATH' ) ) { |
| 9 | exit; |
| 10 | } |
| 11 | |
| 12 | if ( ! function_exists( 'wp_knowledge_types' ) ) { |
| 13 | /** |
| 14 | * Returns the registered knowledge types keyed by slug. |
| 15 | * |
| 16 | * Plugins can register their own types via the `wp_knowledge_types` filter. |
| 17 | * |
| 18 | * @return array { |
| 19 | * Slug-keyed map of knowledge types. |
| 20 | * |
| 21 | * @type array ...$0 { |
| 22 | * Data for a single knowledge type. |
| 23 | * |
| 24 | * @type string $title The human-readable label for the type. |
| 25 | * } |
| 26 | * } |
| 27 | * @phpstan-return array<string, array{title: string}> |
| 28 | */ |
| 29 | function wp_knowledge_types(): array { |
| 30 | /** |
| 31 | * Filters the knowledge types available on this site. |
| 32 | * |
| 33 | * @param array $types { |
| 34 | * Slug-keyed map of knowledge types. |
| 35 | * |
| 36 | * @type array ...$0 { |
| 37 | * Data for a single knowledge type. |
| 38 | * |
| 39 | * @type string $title The human-readable label for the type. |
| 40 | * } |
| 41 | * } |
| 42 | * @phpstan-param array<string, array{title: string}> $types |
| 43 | */ |
| 44 | return apply_filters( |
| 45 | 'wp_knowledge_types', |
| 46 | array( |
| 47 | 'guideline' => array( |
| 48 | 'title' => _x( 'Guideline', 'knowledge type', 'jetpack-knowledge' ), |
| 49 | ), |
| 50 | 'memory' => array( |
| 51 | 'title' => _x( 'Memory', 'knowledge type', 'jetpack-knowledge' ), |
| 52 | ), |
| 53 | 'note' => array( |
| 54 | 'title' => _x( 'Note', 'knowledge type', 'jetpack-knowledge' ), |
| 55 | ), |
| 56 | ) |
| 57 | ); |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | if ( ! function_exists( 'wp_guideline_scopes' ) ) { |
| 62 | /** |
| 63 | * Returns the registered guideline scopes keyed by slug. |
| 64 | * |
| 65 | * @return array { |
| 66 | * Slug-keyed map of guideline scopes. |
| 67 | * |
| 68 | * @type array ...$0 { |
| 69 | * Data for a single scope. |
| 70 | * |
| 71 | * @type string $title Human-readable section title. |
| 72 | * @type string $description Human-readable section description. |
| 73 | * @type int $order Sort order on the Settings page. |
| 74 | * } |
| 75 | * } |
| 76 | * @phpstan-return array<string, array{title: string, description: string, order: int}> |
| 77 | */ |
| 78 | function wp_guideline_scopes(): array { |
| 79 | /** |
| 80 | * Filters the guideline scopes available on this site. |
| 81 | * |
| 82 | * @param array $scopes Slug-keyed map of guideline scopes. |
| 83 | */ |
| 84 | return apply_filters( |
| 85 | 'wp_guideline_scopes', |
| 86 | array( |
| 87 | 'site' => array( |
| 88 | 'title' => __( 'Site', 'jetpack-knowledge' ), |
| 89 | 'description' => __( "Describe your site's purpose, goals, and primary audience.", 'jetpack-knowledge' ), |
| 90 | 'order' => 10, |
| 91 | ), |
| 92 | 'copy' => array( |
| 93 | 'title' => __( 'Copy', 'jetpack-knowledge' ), |
| 94 | 'description' => __( 'Set your writing standards for tone, voice, style, and formatting.', 'jetpack-knowledge' ), |
| 95 | 'order' => 20, |
| 96 | ), |
| 97 | 'images' => array( |
| 98 | 'title' => __( 'Images', 'jetpack-knowledge' ), |
| 99 | 'description' => __( 'Outline your style, dimensions, formats, mood and aesthetic preferences.', 'jetpack-knowledge' ), |
| 100 | 'order' => 30, |
| 101 | ), |
| 102 | 'blocks' => array( |
| 103 | 'title' => __( 'Blocks', 'jetpack-knowledge' ), |
| 104 | 'description' => __( 'Create tailored guidelines for specific block types.', 'jetpack-knowledge' ), |
| 105 | 'order' => 40, |
| 106 | ), |
| 107 | 'additional' => array( |
| 108 | 'title' => __( 'Additional', 'jetpack-knowledge' ), |
| 109 | 'description' => __( 'Add additional guidelines.', 'jetpack-knowledge' ), |
| 110 | 'order' => 50, |
| 111 | ), |
| 112 | ) |
| 113 | ); |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | if ( ! function_exists( 'wp_guideline_max_length' ) ) { |
| 118 | /** |
| 119 | * Returns the maximum length, in characters, of a guideline row's content. |
| 120 | * |
| 121 | * @return int Maximum number of characters allowed in guideline content. |
| 122 | */ |
| 123 | function wp_guideline_max_length(): int { |
| 124 | /** |
| 125 | * Filters the maximum length, in characters, of a guideline row's content. |
| 126 | * |
| 127 | * @param int $max_length Maximum number of characters. Default 5000. |
| 128 | */ |
| 129 | return (int) apply_filters( 'wp_guideline_max_length', 5000 ); |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | if ( ! function_exists( 'wp_knowledge_get_or_create_type_term' ) ) { |
| 134 | /** |
| 135 | * Resolve a `wp_knowledge_type` term by slug, creating it lazily. |
| 136 | * |
| 137 | * @access private |
| 138 | * |
| 139 | * @param string $slug Term slug. |
| 140 | * @return int|null Term ID, or null on failure. |
| 141 | */ |
| 142 | function wp_knowledge_get_or_create_type_term( string $slug ): ?int { |
| 143 | $term = term_exists( $slug, 'wp_knowledge_type' ); |
| 144 | if ( $term ) { |
| 145 | return (int) $term['term_id']; |
| 146 | } |
| 147 | |
| 148 | $switched = switch_to_locale( get_locale() ); |
| 149 | $term = wp_insert_term( $slug, 'wp_knowledge_type' ); |
| 150 | if ( $switched ) { |
| 151 | restore_previous_locale(); |
| 152 | } |
| 153 | |
| 154 | if ( is_wp_error( $term ) ) { |
| 155 | // Another request may have created the term after our existence check. |
| 156 | $term_id = (int) $term->get_error_data( 'term_exists' ); |
| 157 | return 'term_exists' === $term->get_error_code() && $term_id > 0 ? $term_id : null; |
| 158 | } |
| 159 | |
| 160 | return (int) $term['term_id']; |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | if ( ! function_exists( 'wp_knowledge_ensure_default_type_term' ) ) { |
| 165 | /** |
| 166 | * Hook callback for the `save_post_wp_knowledge` action that assigns the knowledge type term. |
| 167 | * |
| 168 | * @access private |
| 169 | * |
| 170 | * @param int $post_id Saved post ID. |
| 171 | */ |
| 172 | function wp_knowledge_ensure_default_type_term( int $post_id ): void { |
| 173 | if ( wp_is_post_revision( $post_id ) ) { |
| 174 | return; |
| 175 | } |
| 176 | |
| 177 | $post = get_post( $post_id ); |
| 178 | if ( ! $post instanceof WP_Post ) { |
| 179 | return; |
| 180 | } |
| 181 | |
| 182 | if ( str_starts_with( $post->post_name, 'guideline-' ) ) { |
| 183 | $term_id = wp_knowledge_get_or_create_type_term( 'guideline' ); |
| 184 | if ( null !== $term_id ) { |
| 185 | wp_set_object_terms( $post_id, $term_id, 'wp_knowledge_type' ); |
| 186 | } |
| 187 | return; |
| 188 | } |
| 189 | |
| 190 | $terms = get_the_terms( $post_id, 'wp_knowledge_type' ); |
| 191 | if ( is_wp_error( $terms ) || ! empty( $terms ) ) { |
| 192 | return; |
| 193 | } |
| 194 | |
| 195 | $term_id = wp_knowledge_get_or_create_type_term( 'note' ); |
| 196 | if ( null !== $term_id ) { |
| 197 | wp_set_object_terms( $post_id, $term_id, 'wp_knowledge_type' ); |
| 198 | } |
| 199 | } |
| 200 | } |
| 201 | |
| 202 | if ( ! function_exists( 'wp_maybe_grant_knowledge_caps' ) ) { |
| 203 | /** |
| 204 | * Filters the user capabilities to grant the `wp_knowledge` post type capabilities as necessary. |
| 205 | * |
| 206 | * @param bool[] $allcaps An array of all the user's capabilities. |
| 207 | * @param string[] $caps Required primitive capabilities for the requested capability. |
| 208 | * @param array $args Arguments that accompany the requested capability check. |
| 209 | * @param WP_User $user The user object. |
| 210 | * @return bool[] Filtered array of the user's capabilities. |
| 211 | */ |
| 212 | function wp_maybe_grant_knowledge_caps( $allcaps, $caps, $args, $user ) { |
| 213 | if ( ! empty( $allcaps['manage_options'] ) ) { |
| 214 | $allcaps['read_knowledge_items'] = true; |
| 215 | $allcaps['edit_knowledge_items'] = true; |
| 216 | $allcaps['edit_others_knowledge_items'] = true; |
| 217 | $allcaps['edit_published_knowledge_items'] = true; |
| 218 | $allcaps['edit_private_knowledge_items'] = true; |
| 219 | $allcaps['publish_knowledge_items'] = true; |
| 220 | $allcaps['delete_knowledge_items'] = true; |
| 221 | $allcaps['delete_others_knowledge_items'] = true; |
| 222 | $allcaps['delete_published_knowledge_items'] = true; |
| 223 | $allcaps['delete_private_knowledge_items'] = true; |
| 224 | $allcaps['read_private_knowledge_items'] = true; |
| 225 | return $allcaps; |
| 226 | } |
| 227 | |
| 228 | if ( empty( $allcaps['edit_posts'] ) ) { |
| 229 | return $allcaps; |
| 230 | } |
| 231 | |
| 232 | // Ambient floor for Contributor+: `read_knowledge_items` clears the |
| 233 | // post-type read check; `edit_knowledge_items` clears the create and |
| 234 | // ownership checks that don't pass a post ID. Per-post primitives |
| 235 | // are granted only in the per-post branch below. |
| 236 | $allcaps['read_knowledge_items'] = true; |
| 237 | $allcaps['edit_knowledge_items'] = true; |
| 238 | |
| 239 | if ( ( ! isset( $args[0] ) || ! isset( $args[2] ) ) ) { |
| 240 | return $allcaps; |
| 241 | } |
| 242 | |
| 243 | if ( ! in_array( $args[0], array( 'edit_post', 'delete_post', 'read_post' ), true ) ) { |
| 244 | return $allcaps; |
| 245 | } |
| 246 | |
| 247 | $post = get_post( $args[2] ); |
| 248 | if ( |
| 249 | ! $post instanceof WP_Post || |
| 250 | 'wp_knowledge' !== $post->post_type || |
| 251 | (int) $post->post_author !== (int) $user->ID |
| 252 | ) { |
| 253 | return $allcaps; |
| 254 | } |
| 255 | |
| 256 | /* |
| 257 | * A trashed row keeps its pre-trash status in `_wp_trash_meta_status`. |
| 258 | */ |
| 259 | $status = $post->post_status; |
| 260 | if ( 'trash' === $status ) { |
| 261 | $status = get_post_meta( $post->ID, '_wp_trash_meta_status', true ); |
| 262 | } |
| 263 | |
| 264 | if ( 'private' !== $status ) { |
| 265 | return $allcaps; |
| 266 | } |
| 267 | |
| 268 | $allcaps['edit_private_knowledge_items'] = true; |
| 269 | $allcaps['delete_knowledge_items'] = true; |
| 270 | $allcaps['delete_private_knowledge_items'] = true; |
| 271 | $allcaps['read_private_knowledge_items'] = true; |
| 272 | |
| 273 | return $allcaps; |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | if ( ! function_exists( 'wp_knowledge_maybe_map_term_label' ) ) { |
| 278 | /** |
| 279 | * Hook callback for the `wp_insert_term_data` filter that swaps a raw knowledge-type slug for its |
| 280 | * human-readable label when WordPress is about to lazily create the term. |
| 281 | * |
| 282 | * @access private |
| 283 | * |
| 284 | * @param array $data Term data to be inserted (keyed by column name). |
| 285 | * @param string $taxonomy Taxonomy slug. |
| 286 | * @return array Possibly modified term data. |
| 287 | */ |
| 288 | function wp_knowledge_maybe_map_term_label( array $data, string $taxonomy ): array { |
| 289 | if ( 'wp_knowledge_type' !== $taxonomy ) { |
| 290 | return $data; |
| 291 | } |
| 292 | |
| 293 | if ( $data['name'] !== $data['slug'] ) { |
| 294 | return $data; |
| 295 | } |
| 296 | |
| 297 | $types = wp_knowledge_types(); |
| 298 | if ( isset( $types[ $data['slug'] ] ) ) { |
| 299 | $data['name'] = $types[ $data['slug'] ]['title']; |
| 300 | } |
| 301 | |
| 302 | return $data; |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | if ( ! function_exists( 'wp_guideline_scope_from_slug' ) ) { |
| 307 | /** |
| 308 | * Resolve the scope that owns a guideline row slug. |
| 309 | * |
| 310 | * @access private |
| 311 | * |
| 312 | * @param string $slug Post slug. |
| 313 | * @return string|null Scope key, or null if the slug is not a registered scope. |
| 314 | */ |
| 315 | function wp_guideline_scope_from_slug( string $slug ): ?string { |
| 316 | if ( ! str_starts_with( $slug, 'guideline-' ) ) { |
| 317 | return null; |
| 318 | } |
| 319 | |
| 320 | $scopes = wp_guideline_scopes(); |
| 321 | $scope = substr( $slug, strlen( 'guideline-' ) ); |
| 322 | |
| 323 | // A slug that matches a registered scope key is that scope. Checking this |
| 324 | // first lets a scope keyed like `block-foo` win over the per-block |
| 325 | // namespace below, instead of being swallowed by the blocks scope. |
| 326 | if ( isset( $scopes[ $scope ] ) ) { |
| 327 | return $scope; |
| 328 | } |
| 329 | |
| 330 | // Otherwise a `guideline-block-<name>` row is a per-block row that belongs |
| 331 | // to the blocks scope while it is registered. A real block name never |
| 332 | // equals a registered scope key, so the check above stays safe. |
| 333 | if ( str_starts_with( $slug, 'guideline-block-' ) && strlen( $slug ) > strlen( 'guideline-block-' ) ) { |
| 334 | return isset( $scopes['blocks'] ) ? 'blocks' : null; |
| 335 | } |
| 336 | |
| 337 | return null; |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | if ( ! function_exists( 'wp_knowledge_guard_guideline_row' ) ) { |
| 342 | /** |
| 343 | * Hook callback for `rest_pre_insert_wp_knowledge` that sanitizes and normalizes guideline rows on the |
| 344 | * REST insert path. |
| 345 | * |
| 346 | * @access private |
| 347 | * |
| 348 | * @param stdClass $prepared_post Prepared post object. |
| 349 | * @param WP_REST_Request $request Request object. |
| 350 | * @return stdClass Prepared post. |
| 351 | */ |
| 352 | function wp_knowledge_guard_guideline_row( $prepared_post, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable |
| 353 | $slug = ''; |
| 354 | if ( ! empty( $prepared_post->post_name ) ) { |
| 355 | $slug = $prepared_post->post_name; |
| 356 | } elseif ( ! empty( $prepared_post->ID ) ) { |
| 357 | $existing = get_post( $prepared_post->ID ); |
| 358 | if ( $existing instanceof WP_Post ) { |
| 359 | $slug = $existing->post_name; |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | // Only shape rows whose slug maps to a registered guideline scope. |
| 364 | $scope = wp_guideline_scope_from_slug( (string) $slug ); |
| 365 | if ( null === $scope ) { |
| 366 | return $prepared_post; |
| 367 | } |
| 368 | |
| 369 | // Sanitize content: plain text, capped at the guideline length. |
| 370 | if ( isset( $prepared_post->post_content ) ) { |
| 371 | $content = sanitize_textarea_field( $prepared_post->post_content ); |
| 372 | $max = wp_guideline_max_length(); |
| 373 | if ( mb_strlen( $content, 'UTF-8' ) > $max ) { |
| 374 | $content = mb_substr( $content, 0, $max, 'UTF-8' ); |
| 375 | } |
| 376 | $prepared_post->post_content = $content; |
| 377 | } |
| 378 | |
| 379 | // Re-stamp single-row registry scope titles in the site locale. The |
| 380 | // blocks scope is multi-row, so its per-block rows keep the |
| 381 | // client-provided canonical block name. |
| 382 | if ( 'blocks' !== $scope ) { |
| 383 | $switched = switch_to_locale( get_locale() ); |
| 384 | $scopes = wp_guideline_scopes(); |
| 385 | if ( $switched ) { |
| 386 | restore_previous_locale(); |
| 387 | } |
| 388 | if ( isset( $scopes[ $scope ]['title'] ) ) { |
| 389 | $prepared_post->post_title = $scopes[ $scope ]['title']; |
| 390 | } |
| 391 | } |
| 392 | |
| 393 | return $prepared_post; |
| 394 | } |
| 395 | } |