The SEO Framework - Version 4.2.5

Version Description

This minor update addresses a change in WordPress 6.0 that causes taxonomy sitemaps to crash, allows paginated deindexing to supersede forced indexing, and improves image cropping by preserving metadata.

Download this release

Release Info

Developer Cybr
Plugin Icon 128x128 The SEO Framework
Version 4.2.5
Comparing to
See all releases

Code changes from version 4.2.4 to 4.2.5

autodescription.php CHANGED
@@ -3,7 +3,7 @@
3
  * Plugin Name: The SEO Framework
4
  * Plugin URI: https://theseoframework.com/
5
  * Description: An automated, advanced, accessible, unbranded and extremely fast SEO solution for your WordPress website.
6
- * Version: 4.2.4
7
  * Author: The SEO Framework Team
8
  * Author URI: https://theseoframework.com/
9
  * License: GPLv3
@@ -41,7 +41,7 @@ defined( 'ABSPATH' ) or die;
41
  *
42
  * @since 2.3.5
43
  */
44
- define( 'THE_SEO_FRAMEWORK_VERSION', '4.2.4' );
45
 
46
  /**
47
  * The plugin Database version.
3
  * Plugin Name: The SEO Framework
4
  * Plugin URI: https://theseoframework.com/
5
  * Description: An automated, advanced, accessible, unbranded and extremely fast SEO solution for your WordPress website.
6
+ * Version: 4.2.5
7
  * Author: The SEO Framework Team
8
  * Author URI: https://theseoframework.com/
9
  * License: GPLv3
41
  *
42
  * @since 2.3.5
43
  */
44
+ define( 'THE_SEO_FRAMEWORK_VERSION', '4.2.5' );
45
 
46
  /**
47
  * The plugin Database version.
bootstrap/activation.php CHANGED
@@ -71,7 +71,7 @@ function _activation_set_options_autoload() {
71
  \remove_all_filters( "sanitize_option_{$setting}" );
72
 
73
  $temp_options = $options;
74
- //? Write a small difference, so the change will be forwarded to the database.
75
  if ( \is_array( $temp_options ) )
76
  $temp_options['update_buster'] = (int) time();
77
 
71
  \remove_all_filters( "sanitize_option_{$setting}" );
72
 
73
  $temp_options = $options;
74
+ // Write a small difference, so the change will be forwarded to the database.
75
  if ( \is_array( $temp_options ) )
76
  $temp_options['update_buster'] = (int) time();
77
 
bootstrap/deactivation.php CHANGED
@@ -52,7 +52,7 @@ function _deactivation_unset_options_autoload() {
52
  \remove_all_filters( "sanitize_option_{$setting}" );
53
 
54
  $temp_options = $options;
55
- //? Write a small difference, so the change will be forwarded to the database.
56
  if ( \is_array( $temp_options ) )
57
  $temp_options['update_buster'] = (int) time();
58
 
52
  \remove_all_filters( "sanitize_option_{$setting}" );
53
 
54
  $temp_options = $options;
55
+ // Write a small difference, so the change will be forwarded to the database.
56
  if ( \is_array( $temp_options ) )
57
  $temp_options['update_buster'] = (int) time();
58
 
bootstrap/load.php CHANGED
@@ -148,7 +148,7 @@ function _autoload_classes( $class ) {
148
  $_chunck_count = \count( $_chunks );
149
 
150
  if ( $_chunck_count > 2 ) {
151
- //? directory position = $_chunck_count - ( 2 = (The_SEO_Framework)\ + (Bridges/Builders/Interpreters)\ )
152
  $rel_dir = implode( DIRECTORY_SEPARATOR, array_splice( $_chunks, 1, $_chunck_count - 2 ) ) . DIRECTORY_SEPARATOR;
153
  } else {
154
  $rel_dir = '';
148
  $_chunck_count = \count( $_chunks );
149
 
150
  if ( $_chunck_count > 2 ) {
151
+ // directory position = $_chunck_count - ( 2 = (The_SEO_Framework)\ + (Bridges/Builders/Interpreters)\ )
152
  $rel_dir = implode( DIRECTORY_SEPARATOR, array_splice( $_chunks, 1, $_chunck_count - 2 ) ) . DIRECTORY_SEPARATOR;
153
  } else {
154
  $rel_dir = '';
bootstrap/upgrade.php CHANGED
@@ -208,9 +208,9 @@ function _upgrade( $previous_version ) {
208
  $current_version = $previous_version;
209
 
210
  //! From update 3103 henceforth, the upgrade procedures should be backward compatible.
211
- //? This means no data may be erased for at least 1 major version, or 1 year, whichever is later.
212
- //? We must manually delete settings that are no longer used; we merge them otherwise.
213
- //? When a user upgrades beyond this scope, they aren't expected to roll back.
214
  $versions = [ '1', '2701', '2802', '2900', '3001', '3103', '3300', '4051', '4103', '4110', '4120', '4200' ];
215
 
216
  foreach ( $versions as $_version ) {
208
  $current_version = $previous_version;
209
 
210
  //! From update 3103 henceforth, the upgrade procedures should be backward compatible.
211
+ // This means no data may be erased for at least 1 major version, or 1 year, whichever is later.
212
+ // We must manually delete settings that are no longer used; we merge them otherwise.
213
+ // When a user upgrades beyond this scope, they aren't expected to roll back.
214
  $versions = [ '1', '2701', '2802', '2900', '3001', '3103', '3300', '4051', '4103', '4110', '4120', '4200' ];
215
 
216
  foreach ( $versions as $_version ) {
inc/classes/admin-init.class.php CHANGED
@@ -391,11 +391,6 @@ class Admin_Init extends Init {
391
  // Predict white screen:
392
  $headers_sent = headers_sent();
393
 
394
- /**
395
- * Dev debug:
396
- * 1. Change 302 to 500 if you wish to test headers.
397
- * 2. Also force handle_admin_redirect_error() to run.
398
- */
399
  \wp_safe_redirect( $target, 302 );
400
 
401
  // White screen of death for non-debugging users. Let's make it friendlier.
@@ -581,7 +576,7 @@ class Admin_Init extends Init {
581
  public function _dismiss_notice() {
582
 
583
  // phpcs:ignore, WordPress.Security.NonceVerification.Missing -- We require the POST data to find locally stored nonces.
584
- $key = $_POST['tsf-notice-submit'] ?? '';
585
 
586
  if ( ! $key ) return;
587
 
391
  // Predict white screen:
392
  $headers_sent = headers_sent();
393
 
 
 
 
 
 
394
  \wp_safe_redirect( $target, 302 );
395
 
396
  // White screen of death for non-debugging users. Let's make it friendlier.
576
  public function _dismiss_notice() {
577
 
578
  // phpcs:ignore, WordPress.Security.NonceVerification.Missing -- We require the POST data to find locally stored nonces.
579
+ $key = \sanitize_key( $_POST['tsf-notice-submit'] ?? '' );
580
 
581
  if ( ! $key ) return;
582
 
inc/classes/bridges/ajax.class.php CHANGED
@@ -137,7 +137,9 @@ final class AJAX {
137
  * @since 3.1.0 Introduced in 2.9.0, but the name changed.
138
  * @since 4.1.4 Moved to \The_SEO_Framework\Bridges\AJAX and made static.
139
  * @since 4.2.0 Now cleans response header.
140
- * @securitycheck 3.0.0 OK.
 
 
141
  * @access private
142
  */
143
  public static function _wp_ajax_crop_image() {
@@ -153,6 +155,9 @@ final class AJAX {
153
 
154
  $attachment_id = \absint( $_POST['id'] );
155
 
 
 
 
156
  $context = str_replace( '_', '-', \sanitize_key( $_POST['context'] ) );
157
  $data = array_map( 'absint', $_POST['cropDetails'] );
158
  $cropped = \wp_crop_image( $attachment_id, $data['x1'], $data['y1'], $data['width'], $data['height'], $data['dst_width'], $data['dst_height'] );
@@ -175,43 +180,65 @@ final class AJAX {
175
  */
176
  \do_action( 'wp_ajax_crop_image_pre_save', $context, $attachment_id, $cropped );
177
 
178
- /** This filter is documented in wp-admin/custom-header.php */
179
  $cropped = \apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication.
180
 
181
- $parent_url = \wp_get_attachment_url( $attachment_id );
182
- $url = str_replace( basename( $parent_url ), basename( $cropped ), $parent_url );
183
-
184
- // phpcs:ignore, WordPress.PHP.NoSilencedErrors -- Feature may be disabled; should not cause fatal errors.
185
- $size = @getimagesize( $cropped );
186
- $image_type = ( $size ) ? $size['mime'] : 'image/jpeg';
187
-
188
- $object = [
189
- 'post_title' => basename( $cropped ),
190
- 'post_content' => $url,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  'post_mime_type' => $image_type,
192
  'guid' => $url,
193
  'context' => $context,
194
  ];
195
 
196
- $attachment_id = \wp_insert_attachment( $object, $cropped );
 
 
 
 
 
 
 
 
 
 
 
 
197
  $metadata = \wp_generate_attachment_metadata( $attachment_id, $cropped );
198
 
199
  /**
200
- * Filters the cropped image attachment metadata.
201
- *
202
  * @since 4.3.0 WordPress Core
203
  * @see wp_generate_attachment_metadata()
204
- *
205
  * @param array $metadata Attachment metadata.
206
  */
207
  $metadata = \apply_filters( 'wp_ajax_cropped_attachment_metadata', $metadata );
208
  \wp_update_attachment_metadata( $attachment_id, $metadata );
209
 
210
  /**
211
- * Filters the attachment ID for a cropped image.
212
- *
213
  * @since 4.3.0 WordPress Core
214
- *
215
  * @param int $attachment_id The attachment ID of the cropped image.
216
  * @param string $context The Customizer control requesting the cropped image.
217
  */
137
  * @since 3.1.0 Introduced in 2.9.0, but the name changed.
138
  * @since 4.1.4 Moved to \The_SEO_Framework\Bridges\AJAX and made static.
139
  * @since 4.2.0 Now cleans response header.
140
+ * @since 4.2.5 1. Backported cropping support for WebP (WP 5.9).
141
+ * 2. Backported title, description, alt tag, and excerpt preservation (WP 6.0).
142
+ * @securitycheck 4.2.5 OK.
143
  * @access private
144
  */
145
  public static function _wp_ajax_crop_image() {
155
 
156
  $attachment_id = \absint( $_POST['id'] );
157
 
158
+ if ( ! $attachment_id || 'attachment' !== \get_post_type( $attachment_id ) || ! \wp_attachment_is_image( $attachment_id ) )
159
+ \wp_send_json_error( [ 'message' => \esc_js( \__( 'Image could not be processed.', 'default' ) ) ] );
160
+
161
  $context = str_replace( '_', '-', \sanitize_key( $_POST['context'] ) );
162
  $data = array_map( 'absint', $_POST['cropDetails'] );
163
  $cropped = \wp_crop_image( $attachment_id, $data['x1'], $data['y1'], $data['width'], $data['height'], $data['dst_width'], $data['dst_height'] );
180
  */
181
  \do_action( 'wp_ajax_crop_image_pre_save', $context, $attachment_id, $cropped );
182
 
183
+ /** This filter is documented in wp-admin/includes/class-custom-image-header.php */
184
  $cropped = \apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication.
185
 
186
+ $parent_url = \wp_get_attachment_url( $attachment_id );
187
+ $parent_basename = \wp_basename( $parent_url );
188
+ $cropped_basename = \wp_basename( $cropped );
189
+ $url = str_replace( $parent_basename, $cropped_basename, $parent_url );
190
+
191
+ // phpcs:ignore, WordPress.PHP.NoSilencedErrors -- See https://core.trac.wordpress.org/ticket/42480
192
+ $size = \function_exists( '\\wp_getimagesize' ) ? \wp_getimagesize( $cropped ) : @getimagesize( $cropped );
193
+ $image_type = $size ? $size['mime'] : 'image/jpeg';
194
+
195
+ // Get the original image's post to pre-populate the cropped image.
196
+ $original_attachment = \get_post( $attachment_id );
197
+ $sanitized_post_title = \sanitize_file_name( $original_attachment->post_title );
198
+ $use_original_title = (
199
+ \strlen( trim( $original_attachment->post_title ) ) &&
200
+ /**
201
+ * Check if the original image has a title other than the "filename" default,
202
+ * meaning the image had a title when originally uploaded or its title was edited.
203
+ */
204
+ ( $parent_basename !== $sanitized_post_title ) &&
205
+ ( pathinfo( $parent_basename, PATHINFO_FILENAME ) !== $sanitized_post_title )
206
+ );
207
+ $use_original_description = \strlen( trim( $original_attachment->post_content ) );
208
+
209
+ $attachment = [
210
+ 'post_title' => $use_original_title ? $original_attachment->post_title : $cropped_basename,
211
+ 'post_content' => $use_original_description ? $original_attachment->post_content : $url,
212
  'post_mime_type' => $image_type,
213
  'guid' => $url,
214
  'context' => $context,
215
  ];
216
 
217
+ // Copy the image caption attribute (post_excerpt field) from the original image.
218
+ if ( \strlen( trim( $original_attachment->post_excerpt ) ) ) {
219
+ $attachment['post_excerpt'] = $original_attachment->post_excerpt;
220
+ }
221
+
222
+ // Copy the image alt text attribute from the original image.
223
+ if ( \strlen( trim( $original_attachment->_wp_attachment_image_alt ) ) ) {
224
+ $attachment['meta_input'] = [
225
+ '_wp_attachment_image_alt' => \wp_slash( $original_attachment->_wp_attachment_image_alt ),
226
+ ];
227
+ }
228
+
229
+ $attachment_id = \wp_insert_attachment( $attachment, $cropped );
230
  $metadata = \wp_generate_attachment_metadata( $attachment_id, $cropped );
231
 
232
  /**
 
 
233
  * @since 4.3.0 WordPress Core
234
  * @see wp_generate_attachment_metadata()
 
235
  * @param array $metadata Attachment metadata.
236
  */
237
  $metadata = \apply_filters( 'wp_ajax_cropped_attachment_metadata', $metadata );
238
  \wp_update_attachment_metadata( $attachment_id, $metadata );
239
 
240
  /**
 
 
241
  * @since 4.3.0 WordPress Core
 
242
  * @param int $attachment_id The attachment ID of the cropped image.
243
  * @param string $context The Customizer control requesting the cropped image.
244
  */
inc/classes/bridges/listtable.class.php CHANGED
@@ -200,7 +200,7 @@ abstract class ListTable {
200
  $taxonomy = isset( $_POST['taxonomy'] ) ? stripslashes( $_POST['taxonomy'] ) : '';
201
  $post_type = isset( $_POST['post_type'] ) ? stripslashes( $_POST['post_type'] ) : '';
202
 
203
- //? /wp-admin/js/inline-edit-tax.js doesn't send post_type, instead, it sends tax_type, which is the same.
204
  $post_type = $post_type
205
  ?: ( isset( $_POST['tax_type'] ) ? stripslashes( $_POST['tax_type'] ) : '' );
206
 
200
  $taxonomy = isset( $_POST['taxonomy'] ) ? stripslashes( $_POST['taxonomy'] ) : '';
201
  $post_type = isset( $_POST['post_type'] ) ? stripslashes( $_POST['post_type'] ) : '';
202
 
203
+ // /wp-admin/js/inline-edit-tax.js doesn't send post_type, instead, it sends tax_type, which is the same.
204
  $post_type = $post_type
205
  ?: ( isset( $_POST['tax_type'] ) ? stripslashes( $_POST['tax_type'] ) : '' );
206
 
inc/classes/builders/coresitemaps/taxonomies.class.php CHANGED
@@ -40,6 +40,7 @@ class Taxonomies extends \WP_Sitemaps_Taxonomies {
40
  * @since 4.1.2
41
  * @since 4.2.0 Renamed `$taxonomy` to `$object_subtype` to match parent class
42
  * for PHP 8 named parameter support. (Backport WP 5.9)
 
43
  * @source \WP_Sitemaps_Taxonomies\get_url_list()
44
  * @TEMP https://wordpress.slack.com/archives/CTKTGNJJW/p1604995479019700
45
  * @link <https://core.trac.wordpress.org/ticket/51860>
@@ -82,21 +83,22 @@ class Taxonomies extends \WP_Sitemaps_Taxonomies {
82
 
83
  $url_list = [];
84
 
85
- $main = Main::get_instance();
86
-
87
  // Offset by how many terms should be included in previous pages.
88
  $offset = ( $page_num - 1 ) * \wp_sitemaps_get_max_urls( $this->object_type );
89
 
90
  $args = $this->get_taxonomies_query_args( $taxonomy );
 
91
  $args['offset'] = $offset;
92
 
93
  $taxonomy_terms = new \WP_Term_Query( $args );
94
 
 
 
95
  foreach ( $taxonomy_terms->terms ?? [] as $term ) :
96
  /**
97
  * @augmented This if-statement prevents including the term in the sitemap when conditions apply.
98
  */
99
- if ( ! $main->is_term_included_in_sitemap( $term, $taxonomy ) )
100
  continue;
101
 
102
  $term_link = \get_term_link( $term, $taxonomy );
@@ -112,12 +114,14 @@ class Taxonomies extends \WP_Sitemaps_Taxonomies {
112
  * Filters the sitemap entry for an individual term.
113
  *
114
  * @since WP Core 5.5.0
 
115
  *
116
  * @param array $sitemap_entry Sitemap entry for the term.
117
- * @param WP_Term $term Term object.
118
  * @param string $taxonomy Taxonomy name.
 
119
  */
120
- $sitemap_entry = \apply_filters( 'wp_sitemaps_taxonomies_entry', $sitemap_entry, $term, $taxonomy );
121
  $url_list[] = $sitemap_entry;
122
  endforeach;
123
 
40
  * @since 4.1.2
41
  * @since 4.2.0 Renamed `$taxonomy` to `$object_subtype` to match parent class
42
  * for PHP 8 named parameter support. (Backport WP 5.9)
43
+ * @since 4.2.5 Added 'all' fields to the query, allowing caching of terms (Backport WP 6.0).
44
  * @source \WP_Sitemaps_Taxonomies\get_url_list()
45
  * @TEMP https://wordpress.slack.com/archives/CTKTGNJJW/p1604995479019700
46
  * @link <https://core.trac.wordpress.org/ticket/51860>
83
 
84
  $url_list = [];
85
 
 
 
86
  // Offset by how many terms should be included in previous pages.
87
  $offset = ( $page_num - 1 ) * \wp_sitemaps_get_max_urls( $this->object_type );
88
 
89
  $args = $this->get_taxonomies_query_args( $taxonomy );
90
+ $args['fields'] = 'all'; // On WP<6.0 this is 'ids'; overwrite it. This line is a mirror of WPv6.0, too.
91
  $args['offset'] = $offset;
92
 
93
  $taxonomy_terms = new \WP_Term_Query( $args );
94
 
95
+ $main = Main::get_instance();
96
+
97
  foreach ( $taxonomy_terms->terms ?? [] as $term ) :
98
  /**
99
  * @augmented This if-statement prevents including the term in the sitemap when conditions apply.
100
  */
101
+ if ( ! $main->is_term_included_in_sitemap( $term->term_id, $taxonomy ) )
102
  continue;
103
 
104
  $term_link = \get_term_link( $term, $taxonomy );
114
  * Filters the sitemap entry for an individual term.
115
  *
116
  * @since WP Core 5.5.0
117
+ * @since WP Core 6.0.0 Added `$term` argument containing the term object.
118
  *
119
  * @param array $sitemap_entry Sitemap entry for the term.
120
+ * @param int $term_id Term ID.
121
  * @param string $taxonomy Taxonomy name.
122
+ * @param WP_Term $term Term object.
123
  */
124
+ $sitemap_entry = \apply_filters( 'wp_sitemaps_taxonomies_entry', $sitemap_entry, $term->term_id, $taxonomy, $term );
125
  $url_list[] = $sitemap_entry;
126
  endforeach;
127
 
inc/classes/builders/robots/query.class.php CHANGED
@@ -44,6 +44,8 @@ final class Query extends Factory {
44
  * Yields true when "noindex/noarchive/nofollow", yields false when "index/archive/follow".
45
  *
46
  * @since 4.2.0
 
 
47
  * @generator
48
  *
49
  * @param string $type The robots generator type (noindex, nofollow...).
@@ -93,16 +95,9 @@ final class Query extends Factory {
93
 
94
  if ( $tsf->is_real_front_page() ) {
95
  yield 'globals_homepage' => (bool) $tsf->get_option( "homepage_$type" );
96
-
97
- if ( ! ( static::$options & \The_SEO_Framework\ROBOTS_IGNORE_PROTECTION ) )
98
- $asserting_noindex and yield from static::assert_noindex_query_pass( 'paged_home' );
99
  } else {
100
  $asserting_noindex and yield from static::assert_noindex_query_pass( '404' );
101
 
102
- if ( ! ( static::$options & \The_SEO_Framework\ROBOTS_IGNORE_PROTECTION ) )
103
- if ( $asserting_noindex && ( $tsf->is_archive() || $tsf->is_singular_archive() ) )
104
- yield from static::assert_noindex_query_pass( 'paged' );
105
-
106
  if ( $tsf->is_archive() ) {
107
  if ( $tsf->is_author() ) {
108
  yield 'globals_author' => (bool) $tsf->get_option( "author_$type" );
@@ -134,21 +129,22 @@ final class Query extends Factory {
134
 
135
  // We assert options here for a jump to index_protection might be unaware.
136
  index_protection: if ( $asserting_noindex && ! ( static::$options & \The_SEO_Framework\ROBOTS_IGNORE_PROTECTION ) ) {
137
- if ( $tsf->is_singular() ) {
138
- // A reiteration of the very same code as above... but, homepage may not always be singular.
139
- // The conditions below MUST overwrite this, too. So, this is the perfect placement.
140
- if ( $tsf->is_real_front_page() )
141
- yield from static::assert_noindex_query_pass( 'paged_home' );
142
-
143
- yield from static::assert_noindex_query_pass( 'protected' );
144
 
145
- /**
146
- * N.B. WordPress protects this query variable with options 'page_comments'
147
- * and 'default_comments_page' via `redirect_canonical()`, so we don't have to.
148
- * For reference, it fires `remove_query_arg( 'cpage', $redirect['query'] )`;
149
- */
150
- if ( (int) \get_query_var( 'cpage', 0 ) > 0 )
151
- yield from static::assert_noindex_query_pass( 'cpage' );
 
152
  }
153
  }
154
 
44
  * Yields true when "noindex/noarchive/nofollow", yields false when "index/archive/follow".
45
  *
46
  * @since 4.2.0
47
+ * @since 4.2.5 1. Removed needlessly duplicate homepage test.
48
+ * 2. Moved archive pagination check to index_protection.
49
  * @generator
50
  *
51
  * @param string $type The robots generator type (noindex, nofollow...).
95
 
96
  if ( $tsf->is_real_front_page() ) {
97
  yield 'globals_homepage' => (bool) $tsf->get_option( "homepage_$type" );
 
 
 
98
  } else {
99
  $asserting_noindex and yield from static::assert_noindex_query_pass( '404' );
100
 
 
 
 
 
101
  if ( $tsf->is_archive() ) {
102
  if ( $tsf->is_author() ) {
103
  yield 'globals_author' => (bool) $tsf->get_option( "author_$type" );
129
 
130
  // We assert options here for a jump to index_protection might be unaware.
131
  index_protection: if ( $asserting_noindex && ! ( static::$options & \The_SEO_Framework\ROBOTS_IGNORE_PROTECTION ) ) {
132
+ if ( $tsf->is_real_front_page() ) {
133
+ yield from static::assert_noindex_query_pass( 'paged_home' );
134
+ } else {
135
+ if ( $tsf->is_archive() || $tsf->is_singular_archive() ) {
136
+ yield from static::assert_noindex_query_pass( 'paged' );
137
+ } elseif ( $tsf->is_singular() ) {
138
+ yield from static::assert_noindex_query_pass( 'protected' );
139
 
140
+ /**
141
+ * N.B. WordPress protects this query variable with options 'page_comments'
142
+ * and 'default_comments_page' via `redirect_canonical()`, so we don't have to.
143
+ * For reference, it fires `remove_query_arg( 'cpage', $redirect['query'] )`;
144
+ */
145
+ if ( (int) \get_query_var( 'cpage', 0 ) > 0 )
146
+ yield from static::assert_noindex_query_pass( 'cpage' );
147
+ }
148
  }
149
  }
150
 
inc/classes/builders/sitemap/base.class.php CHANGED
@@ -153,6 +153,11 @@ class Base extends Main {
153
  * 3. Renamed method from "generate_sitemap" to abstract extension "build_sitemap".
154
  * 4. Moved to \The_SEO_Framework\Builders\Sitemap\Base
155
  * @abstract
 
 
 
 
 
156
  *
157
  * @return string The sitemap content.
158
  */
@@ -223,6 +228,7 @@ class Base extends Main {
223
  /**
224
  * @since 4.0.0
225
  * @param array $args The query arguments.
 
226
  */
227
  $_args = (array) \apply_filters(
228
  'the_seo_framework_sitemap_hpt_query_args',
153
  * 3. Renamed method from "generate_sitemap" to abstract extension "build_sitemap".
154
  * 4. Moved to \The_SEO_Framework\Builders\Sitemap\Base
155
  * @abstract
156
+ * @slow The queried results are not stored in WP Post's cache, which would allow direct access
157
+ * to all values of the post (if requested). This is because we're using
158
+ * `'fields' => 'ids'` instead of `'fields' => 'all'`. However, this would fill RAM
159
+ * linearly: at 1000 posts, we'd hit 28MB already, 10 000 would be ~280MB, exceeding max.
160
+ * @link <https://w.org/support/topic/sitemap-and-memory-exhaustion/#post-13331896>
161
  *
162
  * @return string The sitemap content.
163
  */
228
  /**
229
  * @since 4.0.0
230
  * @param array $args The query arguments.
231
+ * @link <https://w.org/support/topic/sitemap-and-memory-exhaustion/#post-13331896>
232
  */
233
  $_args = (array) \apply_filters(
234
  'the_seo_framework_sitemap_hpt_query_args',
inc/classes/core.class.php CHANGED
@@ -188,7 +188,7 @@ class Core {
188
  */
189
  public function get_view( $view, $__args = [], $instance = 'main' ) {
190
 
191
- //? A faster extract().
192
  foreach ( $__args as $__k => $__v ) $$__k = $__v;
193
  unset( $__k, $__v, $__args );
194
 
@@ -201,9 +201,11 @@ class Core {
201
  /**
202
  * Stores and returns view secret.
203
  *
204
- * This is not cryptographically secure, but it's enough to fend others off including our files where they shouldn't.
205
- * Our view-files have a certain expectation of inputs to meet. If they don't meet that, we could expose our users to security issues.
206
- * We could not measure any meaningful performance impact by using this (0.02% of 54x get_view() runtime).
 
 
207
  *
208
  * @since 4.1.1
209
  *
@@ -632,7 +634,7 @@ class Core {
632
  $gg = round( $g * .5870 / 8 * $rl );
633
  $gb = round( $b * .1140 / 8 * $rl );
634
 
635
- // Invert grayscela if they hit the relative luminance middle.
636
  if ( $rl < .5 ) {
637
  $gr ^= 0xFF;
638
  $gg ^= 0xFF;
188
  */
189
  public function get_view( $view, $__args = [], $instance = 'main' ) {
190
 
191
+ // A faster extract().
192
  foreach ( $__args as $__k => $__v ) $$__k = $__v;
193
  unset( $__k, $__v, $__args );
194
 
201
  /**
202
  * Stores and returns view secret.
203
  *
204
+ * This is not cryptographically secure, but it's enough to fend others off
205
+ * including our files where they shouldn't. Our view-files have a certain
206
+ * expectation of inputs to meet. When they don't meet that, we could expose
207
+ * our users to security issues. We could not measure any meaningful
208
+ * performance impact by using this (0.02% of 54x get_view() runtime).
209
  *
210
  * @since 4.1.1
211
  *
634
  $gg = round( $g * .5870 / 8 * $rl );
635
  $gb = round( $b * .1140 / 8 * $rl );
636
 
637
+ // Invert grayscela if they pass the relative luminance midpoint.
638
  if ( $rl < .5 ) {
639
  $gr ^= 0xFF;
640
  $gg ^= 0xFF;
inc/classes/detect.class.php CHANGED
@@ -772,6 +772,9 @@ class Detect extends Render {
772
  * Memoizes the return value.
773
  *
774
  * @since 4.2.0
 
 
 
775
  *
776
  * @param string $post_type The post type to test.
777
  * @return bool True if a post is found in the archive, false otherwise.
@@ -965,7 +968,7 @@ class Detect extends Render {
965
  array_unique(
966
  array_merge(
967
  $this->get_forced_supported_post_types(),
968
- //? array_values() because get_post_types() gives a sequential array.
969
  array_keys( (array) \get_post_types( [ 'public' => true ] ) )
970
  )
971
  ),
@@ -1039,7 +1042,7 @@ class Detect extends Render {
1039
  array_unique(
1040
  array_merge(
1041
  $this->get_forced_supported_taxonomies(),
1042
- //? array_values() because get_taxonomies() gives a sequential array.
1043
  array_values( \get_taxonomies( [
1044
  'public' => true,
1045
  '_builtin' => false,
772
  * Memoizes the return value.
773
  *
774
  * @since 4.2.0
775
+ * @slow The queried result is not stored in WP Post's cache, which would allow
776
+ * direct access to all values of the post (if requested). This is because
777
+ * we're using `'fields' => 'ids'` instead of `'fields' => 'all'`.
778
  *
779
  * @param string $post_type The post type to test.
780
  * @return bool True if a post is found in the archive, false otherwise.
968
  array_unique(
969
  array_merge(
970
  $this->get_forced_supported_post_types(),
971
+ // array_values() because get_post_types() gives a sequential array.
972
  array_keys( (array) \get_post_types( [ 'public' => true ] ) )
973
  )
974
  ),
1042
  array_unique(
1043
  array_merge(
1044
  $this->get_forced_supported_taxonomies(),
1045
+ // array_values() because get_taxonomies() gives a sequential array.
1046
  array_values( \get_taxonomies( [
1047
  'public' => true,
1048
  '_builtin' => false,
inc/classes/generate-title.class.php CHANGED
@@ -1033,7 +1033,7 @@ class Generate_Title extends Generate_Description {
1033
  */
1034
  public function get_generated_single_post_title( $id = 0 ) {
1035
 
1036
- //? Home queries can be tricky. Use get_the_real_ID to be certain.
1037
  $_post = \get_post( $id ?: $this->get_the_real_ID() );
1038
 
1039
  if ( isset( $_post->post_title ) ) {
@@ -1458,7 +1458,7 @@ class Generate_Title extends Generate_Description {
1458
  */
1459
  public function use_title_pagination( $args = null ) {
1460
 
1461
- //? Only add pagination if the query is autodetermined, and on a real page.
1462
  if ( null === $args ) {
1463
  if ( \is_404() || \is_admin() ) {
1464
  $use = false;
1033
  */
1034
  public function get_generated_single_post_title( $id = 0 ) {
1035
 
1036
+ // Home queries can be tricky. Use get_the_real_ID to be certain.
1037
  $_post = \get_post( $id ?: $this->get_the_real_ID() );
1038
 
1039
  if ( isset( $_post->post_title ) ) {
1458
  */
1459
  public function use_title_pagination( $args = null ) {
1460
 
1461
+ // Only add pagination if the query is autodetermined, and on a real page.
1462
  if ( null === $args ) {
1463
  if ( \is_404() || \is_admin() ) {
1464
  $use = false;
inc/classes/generate-url.class.php CHANGED
@@ -902,7 +902,7 @@ class Generate_Url extends Generate_Title {
902
  $url = \add_query_arg( [ 'page' => $page ], $url );
903
  }
904
 
905
- //? Append queries other plugins might've filtered.
906
  if ( $this->is_singular() ) {
907
  $url = $this->append_url_query(
908
  $url,
902
  $url = \add_query_arg( [ 'page' => $page ], $url );
903
  }
904
 
905
+ // Append queries other plugins might've filtered.
906
  if ( $this->is_singular() ) {
907
  $url = $this->append_url_query(
908
  $url,
inc/classes/internal/deprecated.class.php CHANGED
@@ -562,7 +562,7 @@ final class Deprecated {
562
  if ( isset( $id ) ) {
563
  $is_shop = (int) \get_option( 'woocommerce_shop_page_id' ) === $id;
564
  } else {
565
- $is_shop = ! \is_admin() && \function_exists( 'is_shop' ) && \is_shop();
566
  }
567
 
568
  return $is_shop;
@@ -595,7 +595,7 @@ final class Deprecated {
595
  if ( $post ) {
596
  $is_product = 'product' === \get_post_type( $post );
597
  } else {
598
- $is_product = \function_exists( 'is_product' ) && \is_product();
599
  }
600
 
601
  return $is_product;
562
  if ( isset( $id ) ) {
563
  $is_shop = (int) \get_option( 'woocommerce_shop_page_id' ) === $id;
564
  } else {
565
+ $is_shop = ! \is_admin() && \function_exists( '\\is_shop' ) && \is_shop();
566
  }
567
 
568
  return $is_shop;
595
  if ( $post ) {
596
  $is_product = 'product' === \get_post_type( $post );
597
  } else {
598
+ $is_product = \function_exists( '\\is_product' ) && \is_product();
599
  }
600
 
601
  return $is_product;
inc/classes/post-data.class.php CHANGED
@@ -202,7 +202,7 @@ class Post_Data extends Detect {
202
  protected function get_unfiltered_post_meta_defaults() {
203
  return [
204
  '_genesis_title' => '',
205
- '_tsf_title_no_blogname' => 0, //? The prefix I should've used from the start...
206
  '_genesis_description' => '',
207
  '_genesis_canonical_uri' => '',
208
  'redirect' => '', //! Will be displayed in custom fields when set...
@@ -552,6 +552,9 @@ class Post_Data extends Detect {
552
  * @since 2.4.3
553
  * @since 2.9.3 1. Removed object caching.
554
  * 2. It now uses WP_Query, instead of wpdb.
 
 
 
555
  *
556
  * @return int Latest Post ID.
557
  */
202
  protected function get_unfiltered_post_meta_defaults() {
203
  return [
204
  '_genesis_title' => '',
205
+ '_tsf_title_no_blogname' => 0, // The prefix I should've used from the start...
206
  '_genesis_description' => '',
207
  '_genesis_canonical_uri' => '',
208
  'redirect' => '', //! Will be displayed in custom fields when set...
552
  * @since 2.4.3
553
  * @since 2.9.3 1. Removed object caching.
554
  * 2. It now uses WP_Query, instead of wpdb.
555
+ * @slow The queried result is not stored in WP Post's cache, which would allow
556
+ * direct access to all values of the post (if requested). This is because
557
+ * we're using `'fields' => 'ids'` instead of `'fields' => 'all'`.
558
  *
559
  * @return int Latest Post ID.
560
  */
inc/classes/sanitize.class.php CHANGED
@@ -99,7 +99,7 @@ class Sanitize extends Admin_Pages {
99
  $this->delete_main_cache();
100
 
101
  // Set backward compatibility. This runs after the sanitization.
102
- \add_filter( 'pre_update_option_' . THE_SEO_FRAMEWORK_SITE_OPTIONS, [ $this, '_set_backward_compatibility' ], 10 );
103
 
104
  // Sets that the options are unchanged, preemptively.
105
  $this->update_static_cache( 'settings_notice', 'unchanged' );
@@ -141,29 +141,15 @@ class Sanitize extends Admin_Pages {
141
  * @since 4.0.0 Emptied and is no longer enqueued.
142
  * @since 4.1.0 1. Added taxonomical robots options backward compat.
143
  * 2. Added the first two parameters.
 
144
  * @access private
145
  *
146
  * @param mixed $new_value The new, unserialized, and filtered option value.
147
  * @return mixed $new_value The updated option.
148
  */
149
  public function _set_backward_compatibility( $new_value ) {
150
-
151
- db_4103:
152
- // Category and Tag robots backward compat.
153
- foreach ( [ 'noindex', 'nofollow', 'noarchive' ] as $r ) :
154
- $robots_option_id = $this->get_robots_taxonomy_option_id( $r );
155
- $new_robots_options = $new_value[ $robots_option_id ] ?? [];
156
-
157
- $new_category_option = $new_robots_options['category'] ?? 0;
158
- $new_tag_option = $new_robots_options['post_tag'] ?? 0;
159
-
160
- // Don't compare to old option--it's never reliably set; it might skip otherwise, although it's always correct.
161
- // Do not resanitize. Others might've overwritten that, let's keep their value.
162
- $new_value[ "category_$r" ] = $new_category_option;
163
- $new_value[ "tag_$r" ] = $new_tag_option;
164
- endforeach;
165
-
166
- end:;
167
  return $new_value;
168
  }
169
 
99
  $this->delete_main_cache();
100
 
101
  // Set backward compatibility. This runs after the sanitization.
102
+ // \add_filter( 'pre_update_option_' . THE_SEO_FRAMEWORK_SITE_OPTIONS, [ $this, '_set_backward_compatibility' ], 10 );
103
 
104
  // Sets that the options are unchanged, preemptively.
105
  $this->update_static_cache( 'settings_notice', 'unchanged' );
141
  * @since 4.0.0 Emptied and is no longer enqueued.
142
  * @since 4.1.0 1. Added taxonomical robots options backward compat.
143
  * 2. Added the first two parameters.
144
+ * @since 4.2.5 Emptied and is no longer enqueued.
145
  * @access private
146
  *
147
  * @param mixed $new_value The new, unserialized, and filtered option value.
148
  * @return mixed $new_value The updated option.
149
  */
150
  public function _set_backward_compatibility( $new_value ) {
151
+ // db_4103:
152
+ // end:;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  return $new_value;
154
  }
155
 
inc/classes/term-data.class.php CHANGED
@@ -402,6 +402,9 @@ class Term_Data extends Post_Data {
402
  * Memoizes the return value.
403
  *
404
  * @since 4.1.0
 
 
 
405
  *
406
  * @return int Latest Category ID.
407
  */
402
  * Memoizes the return value.
403
  *
404
  * @since 4.1.0
405
+ * @slow The queried result is not stored in WP Term's cache, which would allow
406
+ * direct access to all values of the term (if requested). This is because
407
+ * we're using `'fields' => 'ids'` instead of `'fields' => 'all'`.
408
  *
409
  * @return int Latest Category ID.
410
  */
inc/compat/plugin-edd.php CHANGED
@@ -22,7 +22,7 @@ namespace The_SEO_Framework;
22
  function _set_edd_is_product( $is_product, $post ) {
23
 
24
  if ( ! $is_product ) {
25
- if ( \function_exists( 'edd_get_download' ) ) {
26
  $download = \edd_get_download(
27
  $post ? \get_post( $post ) : \tsf()->get_the_real_ID()
28
  );
22
  function _set_edd_is_product( $is_product, $post ) {
23
 
24
  if ( ! $is_product ) {
25
+ if ( \function_exists( '\\edd_get_download' ) ) {
26
  $download = \edd_get_download(
27
  $post ? \get_post( $post ) : \tsf()->get_the_real_ID()
28
  );
inc/compat/plugin-polylang.php CHANGED
@@ -64,7 +64,7 @@ function _polylang_fix_sitemap_base_bath( $path ) {
64
  */
65
  function _polylang_set_sitemap_language() {
66
 
67
- if ( ! \function_exists( 'PLL' ) ) return;
68
  if ( ! ( \PLL() instanceof \PLL_Frontend ) ) return;
69
 
70
  // phpcs:ignore, WordPress.Security.NonceVerification.Recommended -- Arbitrary input expected.
@@ -175,7 +175,7 @@ function _polylang_sitemap_append_non_translatables( $args ) {
175
  * @return string
176
  */
177
  function pll__( $string ) {
178
- if ( \function_exists( 'PLL' ) && \function_exists( '\\pll__' ) )
179
  if ( \PLL() instanceof \PLL_Frontend )
180
  return \pll__( $string );
181
 
@@ -265,8 +265,8 @@ function _polylang_flush_sitemap( $type, $id, $args, $success ) {
265
  )
266
  ); // No cache OK. DB call ok.
267
 
268
- //? We didn't use a wildcard after "_transient_" to reduce scans.
269
- //? A second query is faster on saturated sites.
270
  $wpdb->query(
271
  $wpdb->prepare(
272
  "DELETE FROM $wpdb->options WHERE option_name LIKE %s",
64
  */
65
  function _polylang_set_sitemap_language() {
66
 
67
+ if ( ! \function_exists( '\\PLL' ) ) return;
68
  if ( ! ( \PLL() instanceof \PLL_Frontend ) ) return;
69
 
70
  // phpcs:ignore, WordPress.Security.NonceVerification.Recommended -- Arbitrary input expected.
175
  * @return string
176
  */
177
  function pll__( $string ) {
178
+ if ( \function_exists( '\\PLL' ) && \function_exists( '\\pll__' ) )
179
  if ( \PLL() instanceof \PLL_Frontend )
180
  return \pll__( $string );
181
 
265
  )
266
  ); // No cache OK. DB call ok.
267
 
268
+ // We didn't use a wildcard after "_transient_" to reduce scans.
269
+ // A second query is faster on saturated sites.
270
  $wpdb->query(
271
  $wpdb->prepare(
272
  "DELETE FROM $wpdb->options WHERE option_name LIKE %s",
inc/compat/plugin-woocommerce.php CHANGED
@@ -69,7 +69,7 @@ function _is_shop( $post = null ) {
69
 
70
  $is_shop = (int) \get_option( 'woocommerce_shop_page_id' ) === $id;
71
  } else {
72
- $is_shop = ! \is_admin() && \function_exists( 'is_shop' ) && \is_shop();
73
  }
74
 
75
  return $is_shop;
@@ -146,7 +146,7 @@ function _set_wc_is_product( $is_product, $post ) {
146
  if ( $post ) {
147
  $is_product = 'product' === \get_post_type( $post );
148
  } else {
149
- $is_product = \function_exists( 'is_product' ) && \is_product();
150
  }
151
 
152
  return $is_product;
69
 
70
  $is_shop = (int) \get_option( 'woocommerce_shop_page_id' ) === $id;
71
  } else {
72
+ $is_shop = ! \is_admin() && \function_exists( '\\is_shop' ) && \is_shop();
73
  }
74
 
75
  return $is_shop;
146
  if ( $post ) {
147
  $is_product = 'product' === \get_post_type( $post );
148
  } else {
149
+ $is_product = \function_exists( '\\is_product' ) && \is_product();
150
  }
151
 
152
  return $is_product;
inc/compat/plugin-wpml.php CHANGED
@@ -78,8 +78,8 @@ function _wpml_flush_sitemap( $type, $id, $args, $success ) {
78
  )
79
  ); // No cache OK. DB call ok.
80
 
81
- //? We didn't use a wildcard after "_transient_" to reduce scans.
82
- //? A second query is faster on saturated sites.
83
  $wpdb->query(
84
  $wpdb->prepare(
85
  "DELETE FROM $wpdb->options WHERE option_name LIKE %s",
78
  )
79
  ); // No cache OK. DB call ok.
80
 
81
+ // We didn't use a wildcard after "_transient_" to reduce scans.
82
+ // A second query is faster on saturated sites.
83
  $wpdb->query(
84
  $wpdb->prepare(
85
  "DELETE FROM $wpdb->options WHERE option_name LIKE %s",
inc/functions/upgrade-suggestion.php CHANGED
@@ -64,12 +64,12 @@ _prepare( $previous_version, $current_version );
64
  */
65
  function _prepare( $previous_version, $current_version ) {
66
 
67
- //? 0
68
  // phpcs:ignore, WordPress.PHP.StrictComparisons.LooseComparison -- might be mixed types.
69
  if ( $previous_version == $current_version ) return;
70
- //? 1
71
  if ( \defined( 'TSF_DISABLE_SUGGESTIONS' ) && TSF_DISABLE_SUGGESTIONS ) return;
72
- //? 2
73
  if ( ! \is_main_site() ) return;
74
 
75
  // phpcs:disable -- There is no sale, leftover code.
@@ -83,13 +83,13 @@ function _prepare( $previous_version, $current_version ) {
83
  // }
84
  // phpcs:enable
85
 
86
- //? 3a
87
  if ( \defined( 'TSF_EXTENSION_MANAGER_VERSION' ) ) return;
88
 
89
  if ( ! \function_exists( '\\get_plugins' ) )
90
  require_once ABSPATH . 'wp-admin/includes/plugin.php';
91
 
92
- //? 3b
93
  if ( ! empty( \get_plugins()['the-seo-framework-extension-manager/the-seo-framework-extension-manager.php'] ) ) return;
94
 
95
  // phpcs:ignore, TSF.Performance.Opcodes.ShouldHaveNamespaceEscape
64
  */
65
  function _prepare( $previous_version, $current_version ) {
66
 
67
+ // 0
68
  // phpcs:ignore, WordPress.PHP.StrictComparisons.LooseComparison -- might be mixed types.
69
  if ( $previous_version == $current_version ) return;
70
+ // 1
71
  if ( \defined( 'TSF_DISABLE_SUGGESTIONS' ) && TSF_DISABLE_SUGGESTIONS ) return;
72
+ // 2
73
  if ( ! \is_main_site() ) return;
74
 
75
  // phpcs:disable -- There is no sale, leftover code.
83
  // }
84
  // phpcs:enable
85
 
86
+ // 3a
87
  if ( \defined( 'TSF_EXTENSION_MANAGER_VERSION' ) ) return;
88
 
89
  if ( ! \function_exists( '\\get_plugins' ) )
90
  require_once ABSPATH . 'wp-admin/includes/plugin.php';
91
 
92
+ // 3b
93
  if ( ! empty( \get_plugins()['the-seo-framework-extension-manager/the-seo-framework-extension-manager.php'] ) ) return;
94
 
95
  // phpcs:ignore, TSF.Performance.Opcodes.ShouldHaveNamespaceEscape
inc/views/notice/persistent.php CHANGED
@@ -13,6 +13,8 @@ defined( 'THE_SEO_FRAMEWORK_PRESENT' ) and tsf()->_verify_include_secret( $_secr
13
 
14
  if ( ! $message ) return;
15
 
 
 
16
  // Make sure the scripts are loaded.
17
  $this->init_admin_scripts();
18
  The_SEO_Framework\Builders\Scripts::footer_enqueue();
@@ -26,30 +28,32 @@ $button_js = sprintf(
26
  '<a class="hide-if-no-tsf-js tsf-dismiss" href="javascript:;" title="%s" %s></a>',
27
  esc_attr( $dismiss_title ),
28
  HTML::make_data_attributes( [
29
- 'key' => $key,
30
  // Is this the best nonce key key? Capability validation already happened. See `output_dismissible_persistent_notices()`.
31
- 'nonce' => wp_create_nonce( $this->_get_dismiss_notice_nonce_action( $key ) ),
32
  ] )
33
  );
34
  $button_nojs = vsprintf(
35
- '<form action="%s" method="post" id="tsf-dismiss-notice[%s]" class="hide-if-tsf-js">%s</form>',
36
  [
37
  // Register this at removable_query_args? Ignore? No one cares, literally? Does anyone even read this? Hello!? HELLO!?!?
38
- esc_attr( add_query_arg( [ 'tsf-dismissed-notice' => $key ] ) ),
39
- esc_attr( $key ),
40
  implode(
41
  '',
42
  [
43
- wp_nonce_field( $this->_get_dismiss_notice_nonce_action( $key ), 'tsf_notice_nonce', true, false ),
44
- sprintf(
45
- '<button class="tsf-dismiss" type=submit name=tsf-notice-submit id=tsf-notice-submit[%s] value=%s title="%s">%s</button>',
46
- esc_attr( $key ),
47
- esc_attr( $key ),
48
- esc_attr( $dismiss_title ),
49
- sprintf(
50
- '<span class="screen-reader-text">%s</span>',
51
- esc_html( $dismiss_title )
52
- )
 
 
53
  ),
54
  ]
55
  ),
13
 
14
  if ( ! $message ) return;
15
 
16
+ $sanitized_key = sanitize_key( $key );
17
+
18
  // Make sure the scripts are loaded.
19
  $this->init_admin_scripts();
20
  The_SEO_Framework\Builders\Scripts::footer_enqueue();
28
  '<a class="hide-if-no-tsf-js tsf-dismiss" href="javascript:;" title="%s" %s></a>',
29
  esc_attr( $dismiss_title ),
30
  HTML::make_data_attributes( [
31
+ 'key' => $sanitized_key,
32
  // Is this the best nonce key key? Capability validation already happened. See `output_dismissible_persistent_notices()`.
33
+ 'nonce' => wp_create_nonce( $this->_get_dismiss_notice_nonce_action( $sanitized_key ) ),
34
  ] )
35
  );
36
  $button_nojs = vsprintf(
37
+ '<form action="%s" method=post id="tsf-dismiss-notice[%s]" class=hide-if-tsf-js>%s</form>',
38
  [
39
  // Register this at removable_query_args? Ignore? No one cares, literally? Does anyone even read this? Hello!? HELLO!?!?
40
+ esc_attr( add_query_arg( [ 'tsf-dismissed-notice' => $sanitized_key ] ) ),
41
+ $sanitized_key,
42
  implode(
43
  '',
44
  [
45
+ wp_nonce_field( $this->_get_dismiss_notice_nonce_action( $sanitized_key ), 'tsf_notice_nonce', true, false ),
46
+ vsprintf(
47
+ '<button class=tsf-dismiss type=submit name=tsf-notice-submit id=tsf-notice-submit[%s] value=%s title="%s">%s</button>',
48
+ [
49
+ $sanitized_key,
50
+ $sanitized_key,
51
+ esc_attr( $dismiss_title ),
52
+ sprintf(
53
+ '<span class=screen-reader-text>%s</span>',
54
+ esc_html( $dismiss_title )
55
+ ),
56
+ ]
57
  ),
58
  ]
59
  ),
inc/views/settings/metaboxes/title.php CHANGED
@@ -25,7 +25,7 @@ switch ( $this->get_view_instance( 'title', $instance ) ) :
25
  $latest_cat_id = $this->get_latest_category_id();
26
 
27
  // phpcs:ignore, WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- We don't expect users to set scripts in titles.
28
- $post_name = strip_tags( get_the_title( $latest_post_id ) ) ?: __( 'Example Post', 'autodescription' );
29
  $post_title = $this->s_title( $this->hellip_if_over( $post_name, 60 ) );
30
 
31
  $cat_prefix = $this->s_title( _x( 'Category:', 'category archive title prefix', 'default' ) );
@@ -36,7 +36,7 @@ switch ( $this->get_view_instance( 'title', $instance ) ) :
36
  ) );
37
  $cat_title_full = sprintf(
38
  /* translators: 1: Title prefix. 2: Title. */
39
- _x( '%1$s %2$s', 'archive title', 'default' ),
40
  $cat_prefix,
41
  $cat_title
42
  );
25
  $latest_cat_id = $this->get_latest_category_id();
26
 
27
  // phpcs:ignore, WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- We don't expect users to set scripts in titles.
28
+ $post_name = esc_html( strip_tags( get_the_title( $latest_post_id ) ) ?: __( 'Example Post', 'autodescription' ) );
29
  $post_title = $this->s_title( $this->hellip_if_over( $post_name, 60 ) );
30
 
31
  $cat_prefix = $this->s_title( _x( 'Category:', 'category archive title prefix', 'default' ) );
36
  ) );
37
  $cat_title_full = sprintf(
38
  /* translators: 1: Title prefix. 2: Title. */
39
+ esc_html_x( '%1$s %2$s', 'archive title', 'default' ),
40
  $cat_prefix,
41
  $cat_title
42
  );
lib/js/pt.js CHANGED
@@ -313,10 +313,10 @@ window.tsfPT = function( $ ) {
313
  } );
314
 
315
  if ( primaries[ taxonomy ] ) {
316
- //? One has been set previously via this script, reselect it.
317
  makePrimary( taxonomy, primaries[ taxonomy ] );
318
  } else {
319
- //? Select one according to WordPress's term list sorting.
320
  makeFirstPrimary( taxonomy );
321
  }
322
  }
@@ -327,10 +327,10 @@ window.tsfPT = function( $ ) {
327
  } );
328
 
329
  if ( taxonomies[ taxonomy ].primary ) {
330
- //? One has been saved earlier via this script.
331
  makePrimary( taxonomy, taxonomies[ taxonomy ].primary );
332
  } else {
333
- //? Select one according to WordPress's term list sorting.
334
  makeFirstPrimary( taxonomy );
335
  }
336
  }
313
  } );
314
 
315
  if ( primaries[ taxonomy ] ) {
316
+ // One has been set previously via this script, reselect it.
317
  makePrimary( taxonomy, primaries[ taxonomy ] );
318
  } else {
319
+ // Select one according to WordPress's term list sorting.
320
  makeFirstPrimary( taxonomy );
321
  }
322
  }
327
  } );
328
 
329
  if ( taxonomies[ taxonomy ].primary ) {
330
+ // One has been saved earlier via this script.
331
  makePrimary( taxonomy, taxonomies[ taxonomy ].primary );
332
  } else {
333
+ // Select one according to WordPress's term list sorting.
334
  makeFirstPrimary( taxonomy );
335
  }
336
  }
lib/js/tabs.js CHANGED
@@ -115,6 +115,8 @@ window.tsfTabs = function() {
115
  const cacheId = target.name;
116
  const stack = getStack( stackId );
117
 
 
 
118
  const fadeOutTimeout = 125;
119
  const fadeInTimeout = 175;
120
  const fadeCSS = {
@@ -130,8 +132,7 @@ window.tsfTabs = function() {
130
  animationDuration: `${fadeOutTimeout}ms`,
131
  animationTimingFunction: 'cubic-bezier(.54,.12,.90,.60)',
132
  }
133
- };
134
-
135
  const fadeIn = element => { for ( const prop in fadeCSS.fadeIn ) element.style[ prop ] = fadeCSS.fadeIn[ prop ] };
136
  const fadeOut = element => { for ( const prop in fadeCSS.fadeOut ) element.style[ prop ] = fadeCSS.fadeOut[ prop ] };
137
 
@@ -160,7 +161,7 @@ window.tsfTabs = function() {
160
  await new Promise( _resolve => setTimeout( _resolve, fadeInTimeout * 2/3 ) );
161
 
162
  return testTab(); // do not pass newContent!
163
- };
164
  const testTab = async () => {
165
  // Regain this value from a new query, for the toggle's target-cache might've changed.
166
  let newContent = document.getElementById( `${_toggleCache.get( 'target' ).get( cacheId )}-content` );
115
  const cacheId = target.name;
116
  const stack = getStack( stackId );
117
 
118
+ // TODO make this a public API, and apply to media.js. and tsf.resetAjaxLoader et al.?
119
+ // Also add show/hide? -> new tsfUI file? tsfUI.fadeIn( element )
120
  const fadeOutTimeout = 125;
121
  const fadeInTimeout = 175;
122
  const fadeCSS = {
132
  animationDuration: `${fadeOutTimeout}ms`,
133
  animationTimingFunction: 'cubic-bezier(.54,.12,.90,.60)',
134
  }
135
+ }
 
136
  const fadeIn = element => { for ( const prop in fadeCSS.fadeIn ) element.style[ prop ] = fadeCSS.fadeIn[ prop ] };
137
  const fadeOut = element => { for ( const prop in fadeCSS.fadeOut ) element.style[ prop ] = fadeCSS.fadeOut[ prop ] };
138
 
161
  await new Promise( _resolve => setTimeout( _resolve, fadeInTimeout * 2/3 ) );
162
 
163
  return testTab(); // do not pass newContent!
164
+ }
165
  const testTab = async () => {
166
  // Regain this value from a new query, for the toggle's target-cache might've changed.
167
  let newContent = document.getElementById( `${_toggleCache.get( 'target' ).get( cacheId )}-content` );
readme.txt CHANGED
@@ -2,10 +2,10 @@
2
  Contributors: Cybr
3
  Donate link: https://github.com/sponsors/sybrew
4
  Tags: seo, xml sitemap, google search, open graph, schema.org, twitter card, performance, headless
5
- Requires at least: 5.5.0
6
  Tested up to: 6.0
7
  Requires PHP: 7.2.0
8
- Stable tag: 4.2.4
9
  License: GPLv3
10
  License URI: http://www.gnu.org/licenses/gpl-3.0.html
11
 
@@ -247,6 +247,10 @@ If you wish to display breadcrumbs, then your theme should provide this. Alterna
247
 
248
  == Changelog ==
249
 
 
 
 
 
250
  = 4.2.4 =
251
 
252
  This minor update improves image processing, reducing TSF's load impact by roughly 20% when generating metadata. We also added WordPress 6.0 support for image filesizes, making social sharing [even more robust](https://theseoframework.com/?p=3903).
2
  Contributors: Cybr
3
  Donate link: https://github.com/sponsors/sybrew
4
  Tags: seo, xml sitemap, google search, open graph, schema.org, twitter card, performance, headless
5
+ Requires at least: 5.5
6
  Tested up to: 6.0
7
  Requires PHP: 7.2.0
8
+ Stable tag: 4.2.5
9
  License: GPLv3
10
  License URI: http://www.gnu.org/licenses/gpl-3.0.html
11
 
247
 
248
  == Changelog ==
249
 
250
+ = 4.2.5 =
251
+
252
+ This minor update addresses a change in WordPress 6.0 that causes taxonomy sitemaps to crash, allows paginated deindexing to supersede forced indexing, and improves image cropping by [preserving metadata](https://theseoframework.com/?p=3929).
253
+
254
  = 4.2.4 =
255
 
256
  This minor update improves image processing, reducing TSF's load impact by roughly 20% when generating metadata. We also added WordPress 6.0 support for image filesizes, making social sharing [even more robust](https://theseoframework.com/?p=3903).