PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
← All changes | admin/taxonomy/class-taxonomy.php +119 -53 18.0 → trunk View file →
@@ -4,8 +4,9 @@
4 4 *
5 5 * @package WPSEO\Admin
6 6 */
7 7
8 +use Yoast\WP\SEO\Editors\Application\Site\Website_Information_Repository;
8 9 use Yoast\WP\SEO\Presenters\Admin\Alert_Presenter;
9 10
10 11 /**
11 12 * Class that handles the edit boxes on taxonomy edit pages.
@@ -33,12 +34,19 @@
33 34 */
34 35 private $analysis_readability;
35 36
36 37 /**
38 + * Holds the metabox inclusive language analysis instance.
39 + *
40 + * @var WPSEO_Metabox_Analysis_Inclusive_Language
41 + */
42 + private $analysis_inclusive_language;
43 +
44 + /**
37 45 * Class constructor.
38 46 */
39 47 public function __construct() {
40 - $this->taxonomy = $this->get_taxonomy();
48 + $this->taxonomy = $this::get_taxonomy();
41 49
42 50 add_action( 'edit_term', [ $this, 'update_term' ], 99, 3 );
43 51 add_action( 'init', [ $this, 'custom_category_descriptions_allow_html' ] );
44 52 add_action( 'admin_init', [ $this, 'admin_init' ] );
@@ -45,14 +53,17 @@
45 53
46 54 if ( self::is_term_overview( $GLOBALS['pagenow'] ) ) {
47 55 new WPSEO_Taxonomy_Columns();
48 56 }
49 - $this->analysis_seo = new WPSEO_Metabox_Analysis_SEO();
50 - $this->analysis_readability = new WPSEO_Metabox_Analysis_Readability();
57 + $this->analysis_seo = new WPSEO_Metabox_Analysis_SEO();
58 + $this->analysis_readability = new WPSEO_Metabox_Analysis_Readability();
59 + $this->analysis_inclusive_language = new WPSEO_Metabox_Analysis_Inclusive_Language();
51 60 }
52 61
53 62 /**
54 63 * Add hooks late enough for taxonomy object to be available for checks.
64 + *
65 + * @return void
55 66 */
56 67 public function admin_init() {
57 68
58 69 $taxonomy = get_taxonomy( $this->taxonomy );
@@ -60,9 +71,10 @@
60 71 if ( empty( $taxonomy ) || empty( $taxonomy->public ) || ! $this->show_metabox() ) {
61 72 return;
62 73 }
63 74
64 - $this->insert_description_field_editor();
75 + // Adds custom category description editor. Needs a hook that runs before the description field.
76 + add_action( "{$this->taxonomy}_term_edit_form_top", [ $this, 'custom_category_description_editor' ] );
65 77
66 78 add_action( sanitize_text_field( $this->taxonomy ) . '_edit_form', [ $this, 'term_metabox' ], 90, 1 );
67 79 add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
68 80 }
@@ -70,8 +82,10 @@
70 82 /**
71 83 * Show the SEO inputs for term.
72 84 *
73 85 * @param stdClass|WP_Term $term Term to show the edit boxes for.
86 + *
87 + * @return void
74 88 */
75 89 public function term_metabox( $term ) {
76 90 if ( WPSEO_Metabox::is_internet_explorer() ) {
77 91 $this->show_internet_explorer_notice();
@@ -99,9 +113,9 @@
99 113 esc_html__( 'The browser you are currently using is unfortunately rather dated. Since we strive to give you the best experience possible, we no longer support this browser. Instead, please use %1$sFirefox%4$s, %2$sChrome%4$s or %3$sMicrosoft Edge%4$s.', 'wordpress-seo' ),
100 114 '<a href="https://www.mozilla.org/firefox/new/">',
101 115 '<a href="https://www.google.com/chrome/">',
102 116 '<a href="https://www.microsoft.com/windows/microsoft-edge">',
103 - '</a>'
117 + '</a>',
104 118 );
105 119 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped above.
106 120 echo new Alert_Presenter( $content );
107 121
@@ -111,8 +125,10 @@
111 125 /**
112 126 * Queue assets for taxonomy screens.
113 127 *
114 128 * @since 1.5.0
129 + *
130 + * @return void
115 131 */
116 132 public function admin_enqueue_scripts() {
117 133
118 134 $pagenow = $GLOBALS['pagenow'];
@@ -121,25 +137,25 @@
121 137 return;
122 138 }
123 139
124 140 $asset_manager = new WPSEO_Admin_Asset_Manager();
125 - $asset_manager->enqueue_style( 'scoring' );
126 141 $asset_manager->enqueue_style( 'monorepo' );
127 142
128 - $tag_id = filter_input( INPUT_GET, 'tag_ID' );
143 + $tag_id = $this::get_tag_id();
144 +
129 145 if (
130 146 self::is_term_edit( $pagenow )
131 - && ! empty( $tag_id ) // After we drop support for <4.5 this can be removed.
147 + && $tag_id !== null
132 148 ) {
133 149 wp_enqueue_media(); // Enqueue files needed for upload functionality.
134 150
135 151 $asset_manager->enqueue_style( 'metabox-css' );
136 - $asset_manager->enqueue_style( 'scoring' );
152 + if ( $this->analysis_readability->is_enabled() ) {
153 + $asset_manager->enqueue_style( 'scoring' );
154 + }
155 + $asset_manager->enqueue_style( 'ai-generator' );
137 156 $asset_manager->enqueue_script( 'term-edit' );
138 157
139 - $yoast_components_l10n = new WPSEO_Admin_Asset_Yoast_Components_L10n();
140 - $yoast_components_l10n->localize_script( 'term-edit' );
141 -
142 158 /**
143 159 * Remove the emoji script as it is incompatible with both React and any
144 160 * contenteditable fields.
145 161 */
@@ -147,16 +163,19 @@
147 163
148 164 $asset_manager->localize_script( 'term-edit', 'wpseoAdminL10n', WPSEO_Utils::get_admin_l10n() );
149 165
150 166 $script_data = [
151 - 'analysis' => [
167 + 'analysis' => [
152 168 'plugins' => [
153 169 'replaceVars' => [
154 - 'no_parent_text' => __( '(no parent)', 'wordpress-seo' ),
155 170 'replace_vars' => $this->get_replace_vars(),
156 171 'recommended_replace_vars' => $this->get_recommended_replace_vars(),
157 172 'scope' => $this->determine_scope(),
158 173 ],
174 + 'shortcodes' => [
175 + 'wpseo_shortcode_tags' => $this->get_valid_shortcode_tags(),
176 + 'wpseo_filter_shortcodes_nonce' => wp_create_nonce( 'wpseo-filter-shortcodes' ),
177 + ],
159 178 ],
160 179 'worker' => [
161 180 'url' => YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-analysis-worker' ),
162 181 'dependencies' => YoastSEO()->helpers->asset->get_dependency_urls_by_handle( 'yoast-seo-analysis-worker' ),
@@ -163,22 +182,31 @@
163 182 'keywords_assessment_url' => YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-used-keywords-assessment' ),
164 183 'log_level' => WPSEO_Utils::get_analysis_worker_log_level(),
165 184 ],
166 185 ],
167 - 'media' => [
168 - // @todo replace this translation with JavaScript translations.
169 - 'choose_image' => __( 'Use Image', 'wordpress-seo' ),
170 - ],
171 - 'metabox' => $this->localize_term_scraper_script(),
172 - 'userLanguageCode' => WPSEO_Language_Utils::get_language( \get_user_locale() ),
173 - 'isTerm' => true,
186 + 'metabox' => $this->localize_term_scraper_script( $tag_id ),
187 + 'isTerm' => true,
188 + 'postId' => $tag_id,
189 + 'postType' => $this->get_taxonomy(),
190 + 'usedKeywordsNonce' => wp_create_nonce( 'wpseo-keyword-usage' ),
174 191 ];
192 +
193 + /**
194 + * The website information repository.
195 + *
196 + * @var Website_Information_Repository $repo
197 + */
198 + $repo = YoastSEO()->classes->get( Website_Information_Repository::class );
199 + $term_information = $repo->get_term_site_information();
200 + $term_information->set_term( get_term_by( 'id', $tag_id, $this::get_taxonomy() ) );
201 + $script_data = array_merge_recursive( $term_information->get_legacy_site_information(), $script_data );
202 +
175 203 $asset_manager->localize_script( 'term-edit', 'wpseoScriptData', $script_data );
176 - $asset_manager->enqueue_user_language_script();
177 204 }
178 205
179 206 if ( self::is_term_overview( $pagenow ) ) {
180 207 $asset_manager->enqueue_script( 'edit-page' );
208 + $asset_manager->enqueue_style( 'edit-page' );
181 209 }
182 210 }
183 211
184 212 /**
@@ -186,8 +214,10 @@
186 214 *
187 215 * @param int $term_id ID of the term to save data for.
188 216 * @param int $tt_id The taxonomy_term_id for the term.
189 217 * @param string $taxonomy The taxonomy the term belongs to.
218 + *
219 + * @return void
190 220 */
191 221 public function update_term( $term_id, $tt_id, $taxonomy ) {
192 222 // Bail if this is a multisite installation and the site has been switched.
193 223 if ( is_multisite() && ms_is_switched() ) {
@@ -196,11 +226,13 @@
196 226
197 227 /* Create post array with only our values. */
198 228 $new_meta_data = [];
199 229 foreach ( WPSEO_Taxonomy_Meta::$defaults_per_term as $key => $default ) {
200 - $posted_value = filter_input( INPUT_POST, $key );
201 - if ( isset( $posted_value ) && $posted_value !== false ) {
202 - $new_meta_data[ $key ] = $posted_value;
230 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: Nonce is already checked by WordPress before executing this action.
231 + if ( isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) ) {
232 + // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: $data is getting sanitized later.
233 + $data = wp_unslash( $_POST[ $key ] );
234 + $new_meta_data[ $key ] = ( $key !== 'wpseo_canonical' ) ? WPSEO_Utils::sanitize_text_field( $data ) : WPSEO_Utils::sanitize_url( $data );
203 235 }
204 236
205 237 // If analysis is disabled remove that analysis score value from the DB.
206 238 if ( $this->is_meta_value_disabled( $key ) ) {
@@ -206,9 +238,8 @@
206 238 if ( $this->is_meta_value_disabled( $key ) ) {
207 239 $new_meta_data[ $key ] = '';
208 240 }
209 241 }
210 - unset( $key, $default );
211 242
212 243 // Saving the values.
213 244 WPSEO_Taxonomy_Meta::set_values( $term_id, $taxonomy, $new_meta_data );
214 245 }
@@ -227,13 +258,19 @@
227 258 if ( $key === 'wpseo_content_score' && ! $this->analysis_readability->is_enabled() ) {
228 259 return true;
229 260 }
230 261
262 + if ( $key === 'wpseo_inclusive_language_score' && ! $this->analysis_inclusive_language->is_enabled() ) {
263 + return true;
264 + }
265 +
231 266 return false;
232 267 }
233 268
234 269 /**
235 270 * Allows post-kses-filtered HTML in term descriptions.
271 + *
272 + * @return void
236 273 */
237 274 public function custom_category_descriptions_allow_html() {
238 275 remove_filter( 'term_description', 'wp_kses_data' );
239 276 remove_filter( 'pre_term_description', 'wp_filter_kses' );
@@ -242,8 +279,10 @@
242 279 }
243 280
244 281 /**
245 282 * Output the WordPress editor.
283 + *
284 + * @return void
246 285 */
247 286 public function custom_category_description_editor() {
248 287 wp_editor( '', 'description' );
249 288 }
@@ -250,17 +289,18 @@
250 289
251 290 /**
252 291 * Pass variables to js for use with the term-scraper.
253 292 *
293 + * @param int $term_id The ID of the term to localize the script for.
294 + *
254 295 * @return array
255 296 */
256 - public function localize_term_scraper_script() {
257 - $term_id = filter_input( INPUT_GET, 'tag_ID' );
258 - $term = get_term_by( 'id', $term_id, $this->get_taxonomy() );
297 + public function localize_term_scraper_script( $term_id ) {
298 + $term = get_term_by( 'id', $term_id, $this::get_taxonomy() );
259 299 $taxonomy = get_taxonomy( $term->taxonomy );
260 300
261 301 $term_formatter = new WPSEO_Metabox_Formatter(
262 - new WPSEO_Term_Metabox_Formatter( $taxonomy, $term )
302 + new WPSEO_Term_Metabox_Formatter( $taxonomy, $term ),
263 303 );
264 304
265 305 return $term_formatter->get_values();
266 306 }
@@ -271,9 +311,8 @@
271 311 * @return array
272 312 */
273 313 public function localize_replace_vars_script() {
274 314 return [
275 - 'no_parent_text' => __( '(no parent)', 'wordpress-seo' ),
276 315 'replace_vars' => $this->get_replace_vars(),
277 316 'recommended_replace_vars' => $this->get_recommended_replace_vars(),
278 317 'scope' => $this->determine_scope(),
279 318 ];
@@ -285,9 +324,9 @@
285 324 *
286 325 * @return string String decribing the current scope.
287 326 */
288 327 private function determine_scope() {
289 - $taxonomy = $this->get_taxonomy();
328 + $taxonomy = $this::get_taxonomy();
290 329
291 330 if ( $taxonomy === 'category' ) {
292 331 return 'category';
293 332 }
@@ -323,15 +362,17 @@
323 362
324 363 /**
325 364 * Function to get the labels for the current taxonomy.
326 365 *
327 - * @return object Labels for the current taxonomy.
366 + * @return object|null Labels for the current taxonomy or null if the taxonomy is not set.
328 367 */
329 368 public static function get_labels() {
330 - $term = filter_input( INPUT_GET, 'taxonomy', FILTER_DEFAULT, [ 'options' => [ 'default' => '' ] ] );
331 - $taxonomy = get_taxonomy( $term );
332 -
333 - return $taxonomy->labels;
369 + $term = self::get_taxonomy();
370 + if ( $term !== '' ) {
371 + $taxonomy = get_taxonomy( $term );
372 + return $taxonomy->labels;
373 + }
374 + return null;
334 375 }
335 376
336 377 /**
337 378 * Retrieves a template.
@@ -349,20 +390,42 @@
349 390 * Getting the taxonomy from the URL.
350 391 *
351 392 * @return string
352 393 */
353 - private function get_taxonomy() {
354 - return filter_input( INPUT_GET, 'taxonomy', FILTER_DEFAULT, [ 'options' => [ 'default' => '' ] ] );
394 + private static function get_taxonomy() {
395 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
396 + if ( isset( $_GET['taxonomy'] ) && is_string( $_GET['taxonomy'] ) ) {
397 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
398 + return sanitize_text_field( wp_unslash( $_GET['taxonomy'] ) );
399 + }
400 + return '';
355 401 }
356 402
357 403 /**
404 + * Get the current tag ID from the GET parameters.
405 + *
406 + * @return int|null the tag ID if it exists, null otherwise.
407 + */
408 + private static function get_tag_id() {
409 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
410 + if ( isset( $_GET['tag_ID'] ) && is_string( $_GET['tag_ID'] ) ) {
411 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are casting to an integer.
412 + $tag_id = (int) wp_unslash( $_GET['tag_ID'] );
413 + if ( $tag_id > 0 ) {
414 + return $tag_id;
415 + }
416 + }
417 + return null;
418 + }
419 +
420 + /**
358 421 * Prepares the replace vars for localization.
359 422 *
360 - * @return array The replacement variables.
423 + * @return array<string, string> The replacement variables.
361 424 */
362 425 private function get_replace_vars() {
363 - $term_id = filter_input( INPUT_GET, 'tag_ID' );
364 - $term = get_term_by( 'id', $term_id, $this->get_taxonomy() );
426 + $term_id = $this::get_tag_id();
427 + $term = get_term_by( 'id', $term_id, $this::get_taxonomy() );
365 428
366 429 $cached_replacement_vars = [];
367 430
368 431 $vars_to_cache = [
@@ -390,14 +453,18 @@
390 453
391 454 /**
392 455 * Prepares the recommended replace vars for localization.
393 456 *
394 - * @return array The recommended replacement variables.
457 + * @return array<string> The recommended replacement variables.
395 458 */
396 459 private function get_recommended_replace_vars() {
397 460 $recommended_replace_vars = new WPSEO_Admin_Recommended_Replace_Vars();
398 - $taxonomy = filter_input( INPUT_GET, 'taxonomy' );
461 + $taxonomy = $this::get_taxonomy();
399 462
463 + if ( $taxonomy === '' ) {
464 + return [];
465 + }
466 +
400 467 // What is recommended depends on the current context.
401 468 $page_type = $recommended_replace_vars->determine_for_term( $taxonomy );
402 469
403 470 return $recommended_replace_vars->get_recommended_replacevars_for( $page_type );
@@ -403,19 +470,18 @@
403 470 return $recommended_replace_vars->get_recommended_replacevars_for( $page_type );
404 471 }
405 472
406 473 /**
407 - * Adds custom category description editor.
408 - * Needs a hook that runs before the description field. Prior to WP version 4.5 we need to use edit_form as
409 - * term_edit_form_top was introduced in WP 4.5. This can be removed after <4.5 is no longer supported.
474 + * Returns an array with shortcode tags for all registered shortcodes.
410 475 *
411 - * @return void
476 + * @return array<string> Array with shortcode tags.
412 477 */
413 - private function insert_description_field_editor() {
414 - if ( version_compare( $GLOBALS['wp_version'], '4.5', '<' ) ) {
415 - add_action( "{$this->taxonomy}_edit_form", [ $this, 'custom_category_description_editor' ] );
416 - return;
478 + private function get_valid_shortcode_tags() {
479 + $shortcode_tags = [];
480 +
481 + foreach ( $GLOBALS['shortcode_tags'] as $tag => $description ) {
482 + $shortcode_tags[] = $tag;
417 483 }
418 484
419 - add_action( "{$this->taxonomy}_term_edit_form_top", [ $this, 'custom_category_description_editor' ] );
485 + return $shortcode_tags;
420 486 }
421 487 }