• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / 08ebd1f415cb19b473e076df59deca4d8e2fa445

03 Jun 2026 10:17AM UTC coverage: 53.742% (-4.1%) from 57.809%
08ebd1f415cb19b473e076df59deca4d8e2fa445

Pull #23324

github

web-flow
Merge 2e7c2bfd4 into 8774be1c9
Pull Request #23324: Decouple the block editor sidebar from the metabox hidden fields

9956 of 18388 branches covered (54.14%)

Branch coverage included in aggregate %.

53 of 389 new or added lines in 16 files covered. (13.62%)

10 existing lines in 3 files now uncovered.

37624 of 70146 relevant lines covered (53.64%)

42894.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

15.13
/admin/class-primary-term-admin.php
1
<?php
2
/**
3
 * WPSEO plugin file.
4
 *
5
 * @package WPSEO\Admin
6
 */
7

8
/**
9
 * Adds the UI to change the primary term for a post.
10
 */
11
class WPSEO_Primary_Term_Admin implements WPSEO_WordPress_Integration {
12

13
        /**
14
         * Constructor.
15
         *
16
         * @return void
17
         */
18
        public function register_hooks() {
×
19
                add_filter( 'wpseo_content_meta_section_content', [ $this, 'add_input_fields' ] );
×
20

21
                add_action( 'admin_footer', [ $this, 'wp_footer' ], 10 );
×
22

23
                add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
×
24

NEW
25
                add_action( 'init', [ $this, 'register_primary_term_meta_for_rest' ], 20 );
×
26
        }
27

28
        /**
29
         * Gets the current post ID.
30
         *
31
         * @return int The post ID.
32
         */
33
        protected function get_current_id() {
×
34
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are casting to an integer.
35
                $post_id = isset( $_GET['post'] ) && is_string( $_GET['post'] ) ? (int) wp_unslash( $_GET['post'] ) : 0;
×
36

37
                if ( $post_id === 0 && isset( $GLOBALS['post_ID'] ) ) {
×
38
                        $post_id = (int) $GLOBALS['post_ID'];
×
39
                }
40

41
                return $post_id;
×
42
        }
43

44
        /**
45
         * Registers primary taxonomy meta keys for the REST API.
46
         *
47
         * Called on `init` (priority 20) only when the `wpseo_disable_metabox_in_block_editor`
48
         * filter is active. Registers `_yoast_wpseo_primary_{taxonomy}` for every hierarchical
49
         * taxonomy attached to a public post type so that the block editor can persist primary
50
         * term selections via `core/editor.editPost({ meta: { … } })`.
51
         *
52
         * @return void
53
         */
NEW
54
        public function register_primary_term_meta_for_rest() {
×
NEW
55
                if ( ! apply_filters( 'wpseo_disable_metabox_in_block_editor', false ) ) {
×
NEW
56
                        return;
×
57
                }
58

NEW
59
                $post_types = get_post_types( [ 'public' => true ], 'names' );
×
60

NEW
61
                foreach ( $post_types as $post_type ) {
×
NEW
62
                        $taxonomies = get_object_taxonomies( $post_type, 'objects' );
×
63

NEW
64
                        foreach ( $taxonomies as $taxonomy ) {
×
NEW
65
                                if ( ! $taxonomy->hierarchical ) {
×
NEW
66
                                        continue;
×
67
                                }
68

NEW
69
                                $meta_key = WPSEO_Meta::$meta_prefix . 'primary_' . $taxonomy->name;
×
70

71
                                // object_subtype is intentional here: the same taxonomy can be attached to multiple
72
                                // post types and a primary-term key only makes sense for the post type that actually
73
                                // has that taxonomy. Scoping per post type prevents the REST API from exposing the
74
                                // key on post types where the taxonomy is not registered, which would allow setting
75
                                // a primary term for a taxonomy that doesn't apply.
76
                                // This differs from the global WPSEO_Meta::$meta_fields registration, where the
77
                                // standard Yoast fields (title, metadesc, etc.) are valid for every post type.
NEW
78
                                register_meta(
×
NEW
79
                                        'post',
×
NEW
80
                                        $meta_key,
×
NEW
81
                                        [
×
NEW
82
                                                'object_subtype'    => $post_type,
×
NEW
83
                                                'show_in_rest'      => true,
×
NEW
84
                                                'single'            => true,
×
NEW
85
                                                'type'              => 'string',
×
NEW
86
                                                'sanitize_callback' => [ WPSEO_Meta::class, 'sanitize_post_meta' ],
×
NEW
87
                                                'auth_callback'     => static function ( $allowed, $key, $object_id ) {
×
NEW
88
                                                        return current_user_can( 'edit_post', $object_id );
×
NEW
89
                                                },
×
NEW
90
                                        ],
×
NEW
91
                                );
×
92
                        }
93
                }
94
        }
95

96
        /**
97
         * Adds hidden fields for primary taxonomies.
98
         *
99
         * @param string $content The metabox content.
100
         *
101
         * @return string The HTML content.
102
         */
103
        public function add_input_fields( $content ) {
×
104
                $taxonomies = $this->get_primary_term_taxonomies();
×
105

106
                foreach ( $taxonomies as $taxonomy ) {
×
107
                        $content .= $this->primary_term_field( $taxonomy->name );
×
108
                        $content .= wp_nonce_field( 'save-primary-term', WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_nonce', false, false );
×
109
                }
110
                return $content;
×
111
        }
112

113
        /**
114
         * Generates the HTML for a hidden field for a primary taxonomy.
115
         *
116
         * @param string $taxonomy_name The taxonomy's slug.
117
         *
118
         * @return string The HTML for a hidden primary taxonomy field.
119
         */
120
        protected function primary_term_field( $taxonomy_name ) {
×
121
                return sprintf(
×
122
                        '<input class="yoast-wpseo-primary-term" type="hidden" id="%1$s" name="%2$s" value="%3$s" />',
×
123
                        esc_attr( $this->generate_field_id( $taxonomy_name ) ),
×
124
                        esc_attr( $this->generate_field_name( $taxonomy_name ) ),
×
125
                        esc_attr( $this->get_primary_term( $taxonomy_name ) ),
×
126
                );
×
127
        }
128

129
        /**
130
         * Generates an id for a primary taxonomy's hidden field.
131
         *
132
         * @param string $taxonomy_name The taxonomy's slug.
133
         *
134
         * @return string The field id.
135
         */
136
        protected function generate_field_id( $taxonomy_name ) {
×
137
                return 'yoast-wpseo-primary-' . $taxonomy_name;
×
138
        }
139

140
        /**
141
         * Generates a name for a primary taxonomy's hidden field.
142
         *
143
         * @param string $taxonomy_name The taxonomy's slug.
144
         *
145
         * @return string The field id.
146
         */
147
        protected function generate_field_name( $taxonomy_name ) {
×
148
                return WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy_name . '_term';
×
149
        }
150

151
        /**
152
         * Adds primary term templates.
153
         *
154
         * @return void
155
         */
156
        public function wp_footer() {
8✔
157
                $taxonomies = $this->get_primary_term_taxonomies();
8✔
158

159
                if ( ! empty( $taxonomies ) ) {
8✔
160
                        $this->include_js_templates();
4✔
161
                }
162
        }
163

164
        /**
165
         * Enqueues all the assets needed for the primary term interface.
166
         *
167
         * @return void
168
         */
169
        public function enqueue_assets() {
12✔
170
                global $pagenow;
12✔
171

172
                if ( ! WPSEO_Metabox::is_post_edit( $pagenow ) ) {
12✔
173
                        return;
8✔
174
                }
175

176
                $taxonomies = $this->get_primary_term_taxonomies();
4✔
177

178
                // Only enqueue if there are taxonomies that need a primary term.
179
                if ( empty( $taxonomies ) ) {
4✔
180
                        return;
×
181
                }
182

183
                $asset_manager = new WPSEO_Admin_Asset_Manager();
4✔
184
                $asset_manager->enqueue_style( 'primary-category' );
4✔
185

186
                $mapped_taxonomies = $this->get_mapped_taxonomies_for_js( $taxonomies );
4✔
187

188
                $data = [
4✔
189
                        'taxonomies' => $mapped_taxonomies,
4✔
190
                ];
4✔
191

192
                $asset_manager->localize_script( 'post-edit', 'wpseoPrimaryCategoryL10n', $data );
4✔
193
                $asset_manager->localize_script( 'post-edit-classic', 'wpseoPrimaryCategoryL10n', $data );
4✔
194
        }
195

196
        /**
197
         * Gets the id of the primary term.
198
         *
199
         * @param string $taxonomy_name Taxonomy name for the term.
200
         *
201
         * @return int primary term id
202
         */
203
        protected function get_primary_term( $taxonomy_name ) {
×
204
                $primary_term = new WPSEO_Primary_Term( $taxonomy_name, $this->get_current_id() );
×
205

206
                return $primary_term->get_primary_term();
×
207
        }
208

209
        /**
210
         * Returns all the taxonomies for which the primary term selection is enabled.
211
         *
212
         * @param int|null $post_id Default current post ID.
213
         * @return array
214
         */
215
        protected function get_primary_term_taxonomies( $post_id = null ) {
×
216
                $post_id ??= $this->get_current_id();
×
217

218
                $taxonomies = wp_cache_get( 'primary_term_taxonomies_' . $post_id, 'wpseo' );
×
219
                if ( $taxonomies !== false ) {
×
220
                        return $taxonomies;
×
221
                }
222

223
                $taxonomies = $this->generate_primary_term_taxonomies( $post_id );
×
224

225
                wp_cache_set( 'primary_term_taxonomies_' . $post_id, $taxonomies, 'wpseo' );
×
226

227
                return $taxonomies;
×
228
        }
229

230
        /**
231
         * Includes templates file.
232
         *
233
         * @return void
234
         */
235
        protected function include_js_templates() {
×
236
                include_once WPSEO_PATH . 'admin/views/js-templates-primary-term.php';
×
237
        }
238

239
        /**
240
         * Generates the primary term taxonomies.
241
         *
242
         * @param int $post_id ID of the post.
243
         *
244
         * @return array
245
         */
246
        protected function generate_primary_term_taxonomies( $post_id ) {
×
247
                $post_type      = get_post_type( $post_id );
×
248
                $all_taxonomies = get_object_taxonomies( $post_type, 'objects' );
×
249
                $all_taxonomies = array_filter( $all_taxonomies, [ $this, 'filter_hierarchical_taxonomies' ] );
×
250

251
                /**
252
                 * Filters which taxonomies for which the user can choose the primary term.
253
                 *
254
                 * @param array  $taxonomies     An array of taxonomy objects that are primary_term enabled.
255
                 * @param string $post_type      The post type for which to filter the taxonomies.
256
                 * @param array  $all_taxonomies All taxonomies for this post types, even ones that don't have primary term
257
                 *                               enabled.
258
                 */
259
                $taxonomies = (array) apply_filters( 'wpseo_primary_term_taxonomies', $all_taxonomies, $post_type, $all_taxonomies );
×
260

261
                return $taxonomies;
×
262
        }
263

264
        /**
265
         * Creates a map of taxonomies for localization.
266
         *
267
         * @param array $taxonomies The taxononmies that should be mapped.
268
         *
269
         * @return array The mapped taxonomies.
270
         */
271
        protected function get_mapped_taxonomies_for_js( $taxonomies ) {
×
272
                return array_map( [ $this, 'map_taxonomies_for_js' ], $taxonomies );
×
273
        }
274

275
        /**
276
         * Returns an array suitable for use in the javascript.
277
         *
278
         * @param stdClass $taxonomy The taxonomy to map.
279
         *
280
         * @return array The mapped taxonomy.
281
         */
282
        private function map_taxonomies_for_js( $taxonomy ) {
×
283
                $primary_term = $this->get_primary_term( $taxonomy->name );
×
284

285
                if ( empty( $primary_term ) ) {
×
286
                        $primary_term = '';
×
287
                }
288

289
                $terms = get_terms(
×
290
                        [
×
291
                                'taxonomy'               => $taxonomy->name,
×
292
                                'update_term_meta_cache' => false,
×
293
                                'fields'                 => 'id=>name',
×
294
                        ],
×
295
                );
×
296

297
                $mapped_terms_for_js = [];
×
298
                foreach ( $terms as $id => $name ) {
×
299
                        $mapped_terms_for_js[] = [
×
300
                                'id'   => $id,
×
301
                                'name' => $name,
×
302
                        ];
×
303
                }
304

305
                return [
×
306
                        'title'         => $taxonomy->labels->singular_name,
×
307
                        'name'          => $taxonomy->name,
×
308
                        'primary'       => $primary_term,
×
309
                        'singularLabel' => $taxonomy->labels->singular_name,
×
310
                        'fieldId'       => $this->generate_field_id( $taxonomy->name ),
×
311
                        'restBase'      => ( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name,
×
312
                        'terms'         => $mapped_terms_for_js,
×
313
                ];
×
314
        }
315

316
        /**
317
         * Returns whether or not a taxonomy is hierarchical.
318
         *
319
         * @param stdClass $taxonomy Taxonomy object.
320
         *
321
         * @return bool
322
         */
323
        private function filter_hierarchical_taxonomies( $taxonomy ) {
×
324
                return (bool) $taxonomy->hierarchical;
×
325
        }
326
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc