PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
← All changes | includes/seo/class-pattern-resolver.php +74 -6 2.0.12.7.0 View file →
@@ -82,8 +82,67 @@
82 82 return self::process($value, self::placeholders_for($post_id));
83 83 }
84 84
85 85 /**
86 + * Sanitize a variable-tag template for storage.
87 + *
88 + * The write-side counterpart of resolve_value(): every template that
89 + * reaches this class has to survive the trip into the database first.
90 + *
91 + * sanitize_text_field() cannot be used for that. Core's
92 + * _sanitize_text_fields() strips percent-encoded characters, looping
93 + * `preg_replace( '/%[a-f0-9]{2}/i', ... )` until nothing matches, so any
94 + * token whose first two characters are hex digits is eaten on save:
95 + * %date% is stored as "te%" and %category% as "tegory%" (#521). They are
96 + * the only two tags in the language that collide, which is why the
97 + * corruption looked arbitrary — %title%, %sitename%, %sep%, %excerpt%,
98 + * %modified% and %author% all pass through core untouched.
99 + *
100 + * This mirrors what core does either side of that percent loop — invalid
101 + * UTF-8 dropped, tags stripped, control characters removed, whitespace
102 + * collapsed — and simply omits the loop itself.
103 + *
104 + * @since 2.1.1
105 + *
106 + * @param string $value Raw template as submitted.
107 + * @param bool $keep_newlines Preserve newlines, as sanitize_textarea_field() does.
108 + * @return string Sanitized template with its %tokens% intact.
109 + */
110 + public static function sanitize_template(string $value, bool $keep_newlines = false): string {
111 + $filtered = wp_check_invalid_utf8($value);
112 +
113 + if (strpos($filtered, '<') !== false) {
114 + $filtered = wp_pre_kses_less_than($filtered);
115 + // Tags out, the text between them kept.
116 + $filtered = wp_strip_all_tags($filtered, false);
117 + $filtered = str_replace("<\n", "&lt;\n", $filtered);
118 + }
119 +
120 + // C0 controls and DEL, less the tab/newline/carriage-return handled below.
121 + $filtered = (string) preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $filtered);
122 +
123 + if (!$keep_newlines) {
124 + $filtered = (string) preg_replace('/[\r\n\t ]+/', ' ', $filtered);
125 + }
126 +
127 + return trim($filtered);
128 + }
129 +
130 + /**
131 + * Sanitize a variable-tag template that may span multiple lines.
132 + *
133 + * The sanitize_textarea_field() counterpart of sanitize_template().
134 + *
135 + * @since 2.1.1
136 + *
137 + * @param string $value Raw template as submitted.
138 + * @return string Sanitized template with its %tokens% and newlines intact.
139 + */
140 + public static function sanitize_template_textarea(string $value): string {
141 + return self::sanitize_template($value, true);
142 + }
143 +
144 + /**
86 145 * Derive a description from raw post content.
87 146 *
88 147 * wp_strip_all_tags() removes HTML but not shortcodes, so a page built with
89 148 * them published its shortcode source as the description — `[woocommerce_cart]`
@@ -106,9 +165,14 @@
106 165 $text = excerpt_remove_blocks($content);
107 166 $text = strip_shortcodes($text);
108 167 $text = wp_strip_all_tags($text);
109 168
110 - return trim(wp_trim_words($text, $words, '...'));
169 + // $words is a WORD cap, but wp_trim_words() counts CHARACTERS on
170 + // th/ja/zh_*, where it would cut to ~25 characters instead of ~25
171 + // words — about six times too short (#687). trim_words() keeps the
172 + // word cap where words are the unit and falls back to a character
173 + // budget where they are not.
174 + return trim(\ThinkRank\Core\Seo_Text::trim_words($text, $words));
111 175 }
112 176
113 177 /**
114 178 * Resolve any variable-tag string against a term's values.
@@ -156,10 +220,12 @@
156 220 '%title%' => $name,
157 221 '%term%' => $name,
158 222 '%sitename%' => get_bloginfo('name'),
159 223 '%sep%' => self::separator(),
224 + // Same locale trap as derive_excerpt(): a word cap here is a
225 + // ~25-character cap on th/ja/zh_* (#687).
160 226 '%excerpt%' => $description !== ''
161 - ? wp_trim_words(wp_strip_all_tags($description), 25, '...')
227 + ? self::derive_excerpt($description)
162 228 : '',
163 229 '%date%' => '',
164 230 '%modified%' => '',
165 231 '%author%' => '',
@@ -194,11 +260,13 @@
194 260 public static function description(int $post_id): string {
195 261 $template = self::template_for($post_id, 'description', self::DEFAULT_DESCRIPTION);
196 262 $description = self::resolve_value($template, $post_id);
197 263
198 - if (strlen($description) > 160) {
199 - $description = wp_trim_words($description, 25, '...');
200 - }
264 + // Measure and cut in CHARACTERS. strlen() counts bytes, so a Thai or
265 + // CJK description tripped this limit at a third of its length, and
266 + // wp_trim_words() then cut by a unit the locale chooses — 25 words in
267 + // English, 25 characters in Thai (#687).
268 + $description = \ThinkRank\Core\Seo_Text::trim_to_length($description);
201 269
202 270 return $description;
203 271 }
204 272
@@ -313,9 +381,9 @@
313 381 $excerpt = '';
314 382 if ($post) {
315 383 $excerpt = !empty($post->post_excerpt)
316 384 ? $post->post_excerpt
317 - : self::derive_excerpt((string) $post->post_content);
385 + : self::derive_excerpt(Builder_Content::visible_content($post));
318 386 }
319 387
320 388 $author_id = (int) get_post_field('post_author', $post_id);
321 389