PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.2
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.2
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
wordpress-seo / admin / taxonomy / class-taxonomy.php

class-taxonomy.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.2, at admin/taxonomy/class-taxonomy.php

488 lines 14.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WPSEO plugin file.
4 *
5 * @package WPSEO\Admin
6 */
7
8 use Yoast\WP\SEO\Editors\Application\Site\Website_Information_Repository;
9 use Yoast\WP\SEO\Presenters\Admin\Alert_Presenter;
10
11 /**
12 * Class that handles the edit boxes on taxonomy edit pages.
13 */
14 class WPSEO_Taxonomy {
15
16 /**
17 * The current active taxonomy.
18 *
19 * @var string
20 */
21 private $taxonomy = '';
22
23 /**
24 * Holds the metabox SEO analysis instance.
25 *
26 * @var WPSEO_Metabox_Analysis_SEO
27 */
28 private $analysis_seo;
29
30 /**
31 * Holds the metabox readability analysis instance.
32 *
33 * @var WPSEO_Metabox_Analysis_Readability
34 */
35 private $analysis_readability;
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 /**
45 * Class constructor.
46 */
47 public function __construct() {
48 $this->taxonomy = $this::get_taxonomy();
49
50 add_action( 'edit_term', [ $this, 'update_term' ], 99, 3 );
51 add_action( 'init', [ $this, 'custom_category_descriptions_allow_html' ] );
52 add_action( 'admin_init', [ $this, 'admin_init' ] );
53
54 if ( self::is_term_overview( $GLOBALS['pagenow'] ) ) {
55 new WPSEO_Taxonomy_Columns();
56 }
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();
60 }
61
62 /**
63 * Add hooks late enough for taxonomy object to be available for checks.
64 *
65 * @return void
66 */
67 public function admin_init() {
68
69 $taxonomy = get_taxonomy( $this->taxonomy );
70
71 if ( empty( $taxonomy ) || empty( $taxonomy->public ) || ! $this->show_metabox() ) {
72 return;
73 }
74
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' ] );
77
78 add_action( sanitize_text_field( $this->taxonomy ) . '_edit_form', [ $this, 'term_metabox' ], 90, 1 );
79 add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
80 }
81
82 /**
83 * Show the SEO inputs for term.
84 *
85 * @param stdClass|WP_Term $term Term to show the edit boxes for.
86 *
87 * @return void
88 */
89 public function term_metabox( $term ) {
90 if ( WPSEO_Metabox::is_internet_explorer() ) {
91 $this->show_internet_explorer_notice();
92 return;
93 }
94
95 $metabox = new WPSEO_Taxonomy_Metabox( $this->taxonomy, $term );
96 $metabox->display();
97 }
98
99 /**
100 * Renders the content for the internet explorer metabox.
101 *
102 * @return void
103 */
104 private function show_internet_explorer_notice() {
105 $product_title = YoastSEO()->helpers->product->get_product_name();
106
107 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: $product_title is hardcoded.
108 printf( '<div id="wpseo_meta" class="postbox yoast wpseo-taxonomy-metabox-postbox"><h2><span>%1$s</span></h2>', $product_title );
109 echo '<div class="inside">';
110
111 $content = sprintf(
112 /* translators: 1: Link start tag to the Firefox website, 2: Link start tag to the Chrome website, 3: Link start tag to the Edge website, 4: Link closing tag. */
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' ),
114 '<a href="https://www.mozilla.org/firefox/new/">',
115 '<a href="https://www.google.com/chrome/">',
116 '<a href="https://www.microsoft.com/windows/microsoft-edge">',
117 '</a>',
118 );
119 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped above.
120 echo new Alert_Presenter( $content );
121
122 echo '</div></div>';
123 }
124
125 /**
126 * Queue assets for taxonomy screens.
127 *
128 * @since 1.5.0
129 *
130 * @return void
131 */
132 public function admin_enqueue_scripts() {
133
134 $pagenow = $GLOBALS['pagenow'];
135
136 if ( ! ( self::is_term_edit( $pagenow ) || self::is_term_overview( $pagenow ) ) ) {
137 return;
138 }
139
140 $asset_manager = new WPSEO_Admin_Asset_Manager();
141 $asset_manager->enqueue_style( 'monorepo' );
142
143 $tag_id = $this::get_tag_id();
144
145 if (
146 self::is_term_edit( $pagenow )
147 && $tag_id !== null
148 ) {
149 wp_enqueue_media(); // Enqueue files needed for upload functionality.
150
151 $asset_manager->enqueue_style( 'metabox-css' );
152 if ( $this->analysis_readability->is_enabled() ) {
153 $asset_manager->enqueue_style( 'scoring' );
154 }
155 $asset_manager->enqueue_style( 'ai-generator' );
156 $asset_manager->enqueue_script( 'term-edit' );
157
158 /**
159 * Remove the emoji script as it is incompatible with both React and any
160 * contenteditable fields.
161 */
162 remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
163
164 $asset_manager->localize_script( 'term-edit', 'wpseoAdminL10n', WPSEO_Utils::get_admin_l10n() );
165
166 $script_data = [
167 'analysis' => [
168 'plugins' => [
169 'replaceVars' => [
170 'replace_vars' => $this->get_replace_vars(),
171 'recommended_replace_vars' => $this->get_recommended_replace_vars(),
172 'scope' => $this->determine_scope(),
173 ],
174 'shortcodes' => [
175 'wpseo_shortcode_tags' => $this->get_valid_shortcode_tags(),
176 'wpseo_filter_shortcodes_nonce' => wp_create_nonce( 'wpseo-filter-shortcodes' ),
177 ],
178 ],
179 'worker' => [
180 'url' => YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-analysis-worker' ),
181 'dependencies' => YoastSEO()->helpers->asset->get_dependency_urls_by_handle( 'yoast-seo-analysis-worker' ),
182 'keywords_assessment_url' => YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-used-keywords-assessment' ),
183 'log_level' => WPSEO_Utils::get_analysis_worker_log_level(),
184 ],
185 ],
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' ),
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
203 $asset_manager->localize_script( 'term-edit', 'wpseoScriptData', $script_data );
204 }
205
206 if ( self::is_term_overview( $pagenow ) ) {
207 $asset_manager->enqueue_script( 'edit-page' );
208 $asset_manager->enqueue_style( 'edit-page' );
209 }
210 }
211
212 /**
213 * Update the taxonomy meta data on save.
214 *
215 * @param int $term_id ID of the term to save data for.
216 * @param int $tt_id The taxonomy_term_id for the term.
217 * @param string $taxonomy The taxonomy the term belongs to.
218 *
219 * @return void
220 */
221 public function update_term( $term_id, $tt_id, $taxonomy ) {
222 // Bail if this is a multisite installation and the site has been switched.
223 if ( is_multisite() && ms_is_switched() ) {
224 return;
225 }
226
227 /* Create post array with only our values. */
228 $new_meta_data = [];
229 foreach ( WPSEO_Taxonomy_Meta::$defaults_per_term as $key => $default ) {
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 );
235 }
236
237 // If analysis is disabled remove that analysis score value from the DB.
238 if ( $this->is_meta_value_disabled( $key ) ) {
239 $new_meta_data[ $key ] = '';
240 }
241 }
242
243 // Saving the values.
244 WPSEO_Taxonomy_Meta::set_values( $term_id, $taxonomy, $new_meta_data );
245 }
246
247 /**
248 * Determines if the given meta value key is disabled.
249 *
250 * @param string $key The key of the meta value.
251 * @return bool Whether the given meta value key is disabled.
252 */
253 public function is_meta_value_disabled( $key ) {
254 if ( $key === 'wpseo_linkdex' && ! $this->analysis_seo->is_enabled() ) {
255 return true;
256 }
257
258 if ( $key === 'wpseo_content_score' && ! $this->analysis_readability->is_enabled() ) {
259 return true;
260 }
261
262 if ( $key === 'wpseo_inclusive_language_score' && ! $this->analysis_inclusive_language->is_enabled() ) {
263 return true;
264 }
265
266 return false;
267 }
268
269 /**
270 * Allows post-kses-filtered HTML in term descriptions.
271 *
272 * @return void
273 */
274 public function custom_category_descriptions_allow_html() {
275 remove_filter( 'term_description', 'wp_kses_data' );
276 remove_filter( 'pre_term_description', 'wp_filter_kses' );
277 add_filter( 'term_description', 'wp_kses_post' );
278 add_filter( 'pre_term_description', 'wp_filter_post_kses' );
279 }
280
281 /**
282 * Output the WordPress editor.
283 *
284 * @return void
285 */
286 public function custom_category_description_editor() {
287 wp_editor( '', 'description' );
288 }
289
290 /**
291 * Pass variables to js for use with the term-scraper.
292 *
293 * @param int $term_id The ID of the term to localize the script for.
294 *
295 * @return array
296 */
297 public function localize_term_scraper_script( $term_id ) {
298 $term = get_term_by( 'id', $term_id, $this::get_taxonomy() );
299 $taxonomy = get_taxonomy( $term->taxonomy );
300
301 $term_formatter = new WPSEO_Metabox_Formatter(
302 new WPSEO_Term_Metabox_Formatter( $taxonomy, $term ),
303 );
304
305 return $term_formatter->get_values();
306 }
307
308 /**
309 * Pass some variables to js for replacing variables.
310 *
311 * @return array
312 */
313 public function localize_replace_vars_script() {
314 return [
315 'replace_vars' => $this->get_replace_vars(),
316 'recommended_replace_vars' => $this->get_recommended_replace_vars(),
317 'scope' => $this->determine_scope(),
318 ];
319 }
320
321 /**
322 * Determines the scope based on the current taxonomy.
323 * This can be used by the replacevar plugin to determine if a replacement needs to be executed.
324 *
325 * @return string String decribing the current scope.
326 */
327 private function determine_scope() {
328 $taxonomy = $this::get_taxonomy();
329
330 if ( $taxonomy === 'category' ) {
331 return 'category';
332 }
333
334 if ( $taxonomy === 'post_tag' ) {
335 return 'tag';
336 }
337
338 return 'term';
339 }
340
341 /**
342 * Determines if a given page is the term overview page.
343 *
344 * @param string $page The string to check for the term overview page.
345 *
346 * @return bool
347 */
348 public static function is_term_overview( $page ) {
349 return $page === 'edit-tags.php';
350 }
351
352 /**
353 * Determines if a given page is the term edit page.
354 *
355 * @param string $page The string to check for the term edit page.
356 *
357 * @return bool
358 */
359 public static function is_term_edit( $page ) {
360 return $page === 'term.php';
361 }
362
363 /**
364 * Function to get the labels for the current taxonomy.
365 *
366 * @return object|null Labels for the current taxonomy or null if the taxonomy is not set.
367 */
368 public static function get_labels() {
369 $term = self::get_taxonomy();
370 if ( $term !== '' ) {
371 $taxonomy = get_taxonomy( $term );
372 return $taxonomy->labels;
373 }
374 return null;
375 }
376
377 /**
378 * Retrieves a template.
379 * Check if metabox for current taxonomy should be displayed.
380 *
381 * @return bool
382 */
383 private function show_metabox() {
384 $option_key = 'display-metabox-tax-' . $this->taxonomy;
385
386 return WPSEO_Options::get( $option_key );
387 }
388
389 /**
390 * Getting the taxonomy from the URL.
391 *
392 * @return string
393 */
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 '';
401 }
402
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 /**
421 * Prepares the replace vars for localization.
422 *
423 * @return array<string, string> The replacement variables.
424 */
425 private function get_replace_vars() {
426 $term_id = $this::get_tag_id();
427 $term = get_term_by( 'id', $term_id, $this::get_taxonomy() );
428
429 $cached_replacement_vars = [];
430
431 $vars_to_cache = [
432 'date',
433 'id',
434 'sitename',
435 'sitedesc',
436 'sep',
437 'page',
438 'term_title',
439 'term_description',
440 'term_hierarchy',
441 'category_description',
442 'tag_description',
443 'searchphrase',
444 'currentyear',
445 ];
446
447 foreach ( $vars_to_cache as $var ) {
448 $cached_replacement_vars[ $var ] = wpseo_replace_vars( '%%' . $var . '%%', $term );
449 }
450
451 return $cached_replacement_vars;
452 }
453
454 /**
455 * Prepares the recommended replace vars for localization.
456 *
457 * @return array<string> The recommended replacement variables.
458 */
459 private function get_recommended_replace_vars() {
460 $recommended_replace_vars = new WPSEO_Admin_Recommended_Replace_Vars();
461 $taxonomy = $this::get_taxonomy();
462
463 if ( $taxonomy === '' ) {
464 return [];
465 }
466
467 // What is recommended depends on the current context.
468 $page_type = $recommended_replace_vars->determine_for_term( $taxonomy );
469
470 return $recommended_replace_vars->get_recommended_replacevars_for( $page_type );
471 }
472
473 /**
474 * Returns an array with shortcode tags for all registered shortcodes.
475 *
476 * @return array<string> Array with shortcode tags.
477 */
478 private function get_valid_shortcode_tags() {
479 $shortcode_tags = [];
480
481 foreach ( $GLOBALS['shortcode_tags'] as $tag => $description ) {
482 $shortcode_tags[] = $tag;
483 }
484
485 return $shortcode_tags;
486 }
487 }
488