> $paths Nested keys to decode. * @param bool $with_categories Also decode category labels. * * @return void */ private static function migrate_option(string $option_name, array $paths, bool $with_categories): void { $stored = get_option($option_name, null); if (!is_array($stored)) { return; } $changed = false; foreach ($paths as $path) { if (self::decode_path($stored, $path)) { $changed = true; } } if ($with_categories && isset($stored['categories']) && is_array($stored['categories'])) { foreach ($stored['categories'] as $index => $category) { if (!is_array($category)) { continue; } foreach (['label', 'description'] as $key) { if (self::decode_path($stored, ['categories', (string) $index, $key])) { $changed = true; } } } } if ($changed) { update_option($option_name, $stored, false); } } /** * Decode one nested string in place. * * @param array $data Option array (by reference). * @param array $path Keys from the root. * * @return bool True when the stored string changed. */ private static function decode_path(array &$data, array $path): bool { $ref = &$data; $last = array_pop($path); foreach ($path as $key) { if (!is_array($ref) || !array_key_exists($key, $ref)) { return false; } $ref = &$ref[$key]; } if (!is_array($ref) || !isset($ref[$last]) || !is_string($ref[$last])) { return false; } $decoded = self::decode_text($ref[$last]); if ($decoded === $ref[$last]) { return false; } $ref[$last] = $decoded; return true; } /** * Undo the entities esc_html__() writes. Leaves < / > alone. * * @param string $value Stored string. * * @return string */ public static function decode_text(string $value): string { return str_replace( ['"', ''', ''', '&'], ['"', "'", "'", '&'], $value ); } }