← All changes
|
includes/admin/importers/class-abstract-plugin-exporter.php
+183
-12
1.25.0
→
2.13.0
View file →
| @@ -152,8 +152,26 @@ | ||
| 152 | 152 | return []; |
| 153 | 153 | } |
| 154 | 154 | |
| 155 | 155 | /** |
| 156 | + * Records for a data type outside the fixed set above. | |
| 157 | + * | |
| 158 | + * Nothing to hand over by default. It exists so an exporter can gain a type | |
| 159 | + * (ThinkRank's own export lets Pro register its tables this way) without | |
| 160 | + * every subclass having to reimplement export_chunk()'s chunk writing and | |
| 161 | + * manifest bookkeeping. | |
| 162 | + * | |
| 163 | + * @since 2.2.0 | |
| 164 | + * | |
| 165 | + * @param string $type Data type | |
| 166 | + * @param int $page Page number (1-indexed) | |
| 167 | + * @return array Records | |
| 168 | + */ | |
| 169 | + protected function export_custom_type_page(string $type, int $page): array { | |
| 170 | + return []; | |
| 171 | + } | |
| 172 | + | |
| 173 | + /** | |
| 156 | 174 | * Capture the source plugin's Role Manager assignments: role slug => the |
| 157 | 175 | * capabilities that role holds whose name starts with $prefix. |
| 158 | 176 | * |
| 159 | 177 | * Shared by every exporter because role capabilities live on the roles |
| @@ -197,15 +215,152 @@ | ||
| 197 | 215 | |
| 198 | 216 | /** |
| 199 | 217 | * Convert plugin-specific template variables to literal values |
| 200 | 218 | * |
| 201 | - * @param string $value String potentially containing template variables | |
| 219 | + * Accepts mixed because the input is another plugin's stored data, over | |
| 220 | + * which we have no schema guarantees. Rank Math in particular can hold | |
| 221 | + * booleans inside its options arrays where a template string is expected, | |
| 222 | + * and the `?? ''` at the call sites only guards against a MISSING key — | |
| 223 | + * a present-but-boolean value sailed straight into a string-typed | |
| 224 | + * parameter and fataled the whole migration at the snapshot step. | |
| 225 | + * Implementations MUST start with stringify_template_value(). | |
| 226 | + * | |
| 227 | + * @param mixed $value Value potentially containing template variables | |
| 202 | 228 | * @param int|null $post_id Post ID for context-specific variables |
| 203 | 229 | * @return string Converted string |
| 204 | 230 | */ |
| 205 | - abstract protected function convert_template_variables(string $value, ?int $post_id = null): string; | |
| 231 | + abstract protected function convert_template_variables($value, ?int $post_id = null): string; | |
| 206 | 232 | |
| 207 | 233 | /** |
| 234 | + * Coerce a foreign settings/meta value into a template string. | |
| 235 | + * | |
| 236 | + * Strings pass through; ints and floats are kept as their string form (a | |
| 237 | + * purely numeric title is odd but meaningful); everything else — booleans, | |
| 238 | + * arrays, objects, null — has no sensible reading as a template, so it | |
| 239 | + * becomes '', which downstream already treats as "not set" and replaces | |
| 240 | + * with defaults. Dropping garbage beats failing the migration over it. | |
| 241 | + * | |
| 242 | + * @param mixed $value Raw value from the source plugin's storage. | |
| 243 | + * @return string Usable template string, possibly ''. | |
| 244 | + */ | |
| 245 | + final protected function stringify_template_value($value): string { | |
| 246 | + if (is_string($value)) { | |
| 247 | + return $value; | |
| 248 | + } | |
| 249 | + | |
| 250 | + if (is_int($value) || is_float($value)) { | |
| 251 | + return (string) $value; | |
| 252 | + } | |
| 253 | + | |
| 254 | + return ''; | |
| 255 | + } | |
| 256 | + | |
| 257 | + /** | |
| 258 | + * Placeholder a mapped token travels as while the rest of a template is | |
| 259 | + * resolved literally. A control character no source vocabulary uses, so | |
| 260 | + * no literal resolver matches, strips or splits on it. | |
| 261 | + */ | |
| 262 | + private const TOKEN_MARK = "\x1A"; | |
| 263 | + | |
| 264 | + /** | |
| 265 | + * Convert a per-object (post or term) SEO value into ThinkRank variable | |
| 266 | + * tags wherever ThinkRank resolves the same thing per request, and into | |
| 267 | + * literal text everywhere else. | |
| 268 | + * | |
| 269 | + * A source title such as "%%title%% %%sep%% %%sitename%%" used to be | |
| 270 | + * resolved to literal text at import, so the site name, separator and | |
| 271 | + * post title were frozen as they stood that day: renaming the site or | |
| 272 | + * switching the separator left every imported title behind, and every | |
| 273 | + * exporter wrote " - " whatever separator the source used. Mapped tokens | |
| 274 | + * now become `%title%`, `%sep%`, `%sitename%`… — the tags the metabox | |
| 275 | + * inserts and Pattern_Resolver resolves on every output path — and only | |
| 276 | + * tokens ThinkRank has no per-object equivalent for are resolved here. | |
| 277 | + * | |
| 278 | + * Mapped tokens are masked before `$resolve_rest` runs, so a literal | |
| 279 | + * resolver that knows them (Rank Math's shares ThinkRank's syntax) never | |
| 280 | + * sees them, and unmasked afterwards. A value with no mapped token is | |
| 281 | + * returned exactly as `$resolve_rest` produces it. | |
| 282 | + * | |
| 283 | + * @param string $value Raw source value (already stringified) | |
| 284 | + * @param array $token_map Regex matching a source token => ThinkRank tag | |
| 285 | + * @param callable $resolve_rest fn(string): string — the exporter's literal resolver | |
| 286 | + * @return string ThinkRank value | |
| 287 | + */ | |
| 288 | + final protected function tokenize_object_template(string $value, array $token_map, callable $resolve_rest): string { | |
| 289 | + $tags = []; | |
| 290 | + $masked = $value; | |
| 291 | + | |
| 292 | + if ($value !== '') { | |
| 293 | + foreach ($token_map as $pattern => $tag) { | |
| 294 | + $masked = (string) preg_replace_callback( | |
| 295 | + $pattern, | |
| 296 | + static function () use (&$tags, $tag): string { | |
| 297 | + $index = array_search($tag, $tags, true); | |
| 298 | + if ($index === false) { | |
| 299 | + $tags[] = $tag; | |
| 300 | + $index = count($tags) - 1; | |
| 301 | + } | |
| 302 | + | |
| 303 | + return self::TOKEN_MARK . $index . self::TOKEN_MARK; | |
| 304 | + }, | |
| 305 | + $masked | |
| 306 | + ); | |
| 307 | + } | |
| 308 | + } | |
| 309 | + | |
| 310 | + if (empty($tags)) { | |
| 311 | + return (string) $resolve_rest($value); | |
| 312 | + } | |
| 313 | + | |
| 314 | + $resolved = (string) preg_replace_callback( | |
| 315 | + '/' . self::TOKEN_MARK . '(\d+)' . self::TOKEN_MARK . '/', | |
| 316 | + static fn(array $m): string => $tags[(int) $m[1]] ?? '', | |
| 317 | + (string) $resolve_rest($masked) | |
| 318 | + ); | |
| 319 | + | |
| 320 | + // Tokens resolved to '' (e.g. %%page%%) leave separators with nothing | |
| 321 | + // between or around them; Pattern_Resolver tidies those too, but the | |
| 322 | + // stored value should read cleanly in the metabox. | |
| 323 | + $resolved = trim((string) preg_replace('/\s+/', ' ', $resolved)); | |
| 324 | + $resolved = (string) preg_replace('/%sep%(?:\s*%sep%)+/', '%sep%', $resolved); | |
| 325 | + $resolved = (string) preg_replace('/^(?:%sep%\s*)+|(?:\s*%sep%)+$/', '', $resolved); | |
| 326 | + | |
| 327 | + return trim($resolved); | |
| 328 | + } | |
| 329 | + | |
| 330 | + /** | |
| 331 | + * Token map for tokenize_object_template() from literal source tokens | |
| 332 | + * (`%%title%%`, `%title%`), matched case-insensitively. | |
| 333 | + * | |
| 334 | + * @param array<string,string> $map Source token => ThinkRank tag | |
| 335 | + * @return array<string,string> Regex => ThinkRank tag | |
| 336 | + */ | |
| 337 | + final protected static function literal_token_patterns(array $map): array { | |
| 338 | + $patterns = []; | |
| 339 | + foreach ($map as $token => $tag) { | |
| 340 | + $patterns['/' . preg_quote((string) $token, '/') . '/i'] = $tag; | |
| 341 | + } | |
| 342 | + | |
| 343 | + return $patterns; | |
| 344 | + } | |
| 345 | + | |
| 346 | + /** | |
| 347 | + * Token map for tokenize_object_template() from brace-style variable | |
| 348 | + * names (`title` matches `{{title}}` and `{{ title }}`). | |
| 349 | + * | |
| 350 | + * @param array<string,string> $map Variable name => ThinkRank tag | |
| 351 | + * @return array<string,string> Regex => ThinkRank tag | |
| 352 | + */ | |
| 353 | + final protected static function brace_token_patterns(array $map): array { | |
| 354 | + $patterns = []; | |
| 355 | + foreach ($map as $name => $tag) { | |
| 356 | + $patterns['/\{\{\s*' . preg_quote((string) $name, '/') . '\s*\}\}/i'] = $tag; | |
| 357 | + } | |
| 358 | + | |
| 359 | + return $patterns; | |
| 360 | + } | |
| 361 | + | |
| 362 | + /** | |
| 208 | 363 | * Export a chunk of data and write to snapshot |
| 209 | 364 | * |
| 210 | 365 | * This is the main orchestration method. It calls the appropriate |
| 211 | 366 | * export_*_page() method, writes the chunk via Snapshot_Store, and |
| @@ -219,17 +374,30 @@ | ||
| 219 | 374 | // Reset before the page method runs; it (or its id helper) records the |
| 220 | 375 | // raw fetched-row count here. |
| 221 | 376 | $this->last_page_row_count = null; |
| 222 | 377 | |
| 223 | - $records = match ($type) { | |
| 224 | - 'postmeta' => $this->export_postmeta_page($page), | |
| 225 | - 'termmeta' => $this->export_termmeta_page($page), | |
| 226 | - 'usermeta' => $this->export_usermeta_page($page), | |
| 227 | - 'settings' => $this->export_settings(), | |
| 228 | - 'redirections' => $this->export_redirections_page($page), | |
| 229 | - '404_logs' => $this->export_404_logs_page($page), | |
| 230 | - default => [], | |
| 231 | - }; | |
| 378 | + switch ($type) { | |
| 379 | + case 'postmeta': | |
| 380 | + $records = $this->export_postmeta_page($page); | |
| 381 | + break; | |
| 382 | + case 'termmeta': | |
| 383 | + $records = $this->export_termmeta_page($page); | |
| 384 | + break; | |
| 385 | + case 'usermeta': | |
| 386 | + $records = $this->export_usermeta_page($page); | |
| 387 | + break; | |
| 388 | + case 'settings': | |
| 389 | + $records = $this->export_settings(); | |
| 390 | + break; | |
| 391 | + case 'redirections': | |
| 392 | + $records = $this->export_redirections_page($page); | |
| 393 | + break; | |
| 394 | + case '404_logs': | |
| 395 | + $records = $this->export_404_logs_page($page); | |
| 396 | + break; | |
| 397 | + default: | |
| 398 | + $records = $this->export_custom_type_page($type, $page); | |
| 399 | + } | |
| 232 | 400 | |
| 233 | 401 | $exported_count = count($records); |
| 234 | 402 | |
| 235 | 403 | // Write chunk to snapshot store |
| @@ -339,9 +507,9 @@ | ||
| 339 | 507 | $result = ['noindex' => 0, 'nofollow' => 0]; |
| 340 | 508 | |
| 341 | 509 | // Handle serialized array (Rank Math stores robots as serialized array) |
| 342 | 510 | if (is_string($noindex_value) && is_serialized($noindex_value)) { |
| 343 | - $noindex_value = maybe_unserialize($noindex_value); | |
| 511 | + $noindex_value = Safe_Unserializer::unserialize($noindex_value); | |
| 344 | 512 | } |
| 345 | 513 | |
| 346 | 514 | if (is_array($noindex_value)) { |
| 347 | 515 | // Rank Math format: serialized array with 'noindex', 'nofollow' as values |
| @@ -401,8 +569,9 @@ | ||
| 401 | 569 | // (oembed_cache, revisions, nav menu items, block/template CPTs, …) never |
| 402 | 570 | // enter the snapshot — SEO meta left on them is noise. Filtering in the |
| 403 | 571 | // query (rather than per-record) keeps the chunk-size based has_more |
| 404 | 572 | // pagination in export_chunk() accurate. |
| 573 | + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- table name is $wpdb->prefix plus a literal, and every value is passed as a placeholder replacement. | |
| 405 | 574 | $sql = $wpdb->prepare( |
| 406 | 575 | "SELECT DISTINCT pm.post_id |
| 407 | 576 | FROM {$wpdb->postmeta} pm |
| 408 | 577 | INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id |
| @@ -415,9 +584,11 @@ | ||
| 415 | 584 | $post_types, |
| 416 | 585 | [$this->chunk_size, $offset] |
| 417 | 586 | ) |
| 418 | 587 | ); |
| 588 | + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 419 | 589 | |
| 590 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table name is $wpdb->prefix plus a literal, and every value is passed as a placeholder replacement. | |
| 420 | 591 | $ids = $wpdb->get_col($sql); |
| 421 | 592 | $this->last_page_row_count = count($ids); |
| 422 | 593 | |
| 423 | 594 | return $ids; |