- Import
This commit is contained in:
Executable
+646
@@ -0,0 +1,646 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use NeonBlog\ContentRepository;
|
||||
|
||||
$app = require dirname(__DIR__) . '/app/bootstrap.php';
|
||||
$repo = $app->repository();
|
||||
$comments = $app->comments();
|
||||
|
||||
[$args, $opts] = parseArguments(array_slice($_SERVER['argv'] ?? [], 1));
|
||||
|
||||
if (isset($opts['help']) || isset($opts['h']) || ($args === [] && !isset($opts['source']))) {
|
||||
help();
|
||||
exit(isset($opts['help']) || isset($opts['h']) ? 0 : 1);
|
||||
}
|
||||
|
||||
$source = (string) ($opts['source'] ?? $args[0]);
|
||||
$dryRun = isset($opts['dry-run']);
|
||||
$overwrite = isset($opts['overwrite']);
|
||||
$skipUploads = isset($opts['skip-uploads']);
|
||||
$skipComments = isset($opts['skip-comments']);
|
||||
|
||||
try {
|
||||
$paths = resolveBluditSource($source);
|
||||
$siteDb = readDatabase($paths['databases'] . '/site.php')
|
||||
?: readDatabase($paths['databases'] . '/site.json');
|
||||
$timezone = (string) ($opts['source-timezone'] ?? $siteDb['timezone'] ?? date_default_timezone_get());
|
||||
|
||||
$pagesDb = readDatabase($paths['databases'] . '/pages.php')
|
||||
?: readDatabase($paths['databases'] . '/pages.json');
|
||||
$categoriesDb = readDatabase($paths['databases'] . '/categories.php')
|
||||
?: readDatabase($paths['databases'] . '/categories.json');
|
||||
$tagsDb = readDatabase($paths['databases'] . '/tags.php')
|
||||
?: readDatabase($paths['databases'] . '/tags.json');
|
||||
|
||||
$entries = discoverPages($paths['pages'], $pagesDb);
|
||||
if ($entries === []) {
|
||||
fail('No Bludit pages were found in ' . $paths['pages']);
|
||||
}
|
||||
|
||||
$summary = [
|
||||
'source' => $paths['content'],
|
||||
'dry_run' => $dryRun,
|
||||
'pages_found' => count($entries),
|
||||
'content_imported' => 0,
|
||||
'uploads_copied' => 0,
|
||||
'page_assets_copied' => 0,
|
||||
'comments_imported' => 0,
|
||||
'skipped' => [],
|
||||
];
|
||||
|
||||
$slugMap = [];
|
||||
$reservedSlugs = [];
|
||||
foreach ($entries as $entry) {
|
||||
$oldKey = (string) $entry['key'];
|
||||
$row = is_array($entry['db']) ? $entry['db'] : [];
|
||||
$contentPath = $entry['path'];
|
||||
$markdown = is_file($contentPath) ? (string) file_get_contents($contentPath) : '';
|
||||
$baseSlug = slugFromBluditKey($oldKey);
|
||||
$slug = uniqueSlug($repo, $baseSlug, $reservedSlugs, $overwrite);
|
||||
$reservedSlugs[$slug] = true;
|
||||
$metadata = metadataFromBludit($oldKey, $row, $markdown, $categoriesDb, $tagsDb, $timezone);
|
||||
|
||||
$slugMap[$oldKey] = $slug;
|
||||
if (!$dryRun) {
|
||||
$repo->saveImported($slug, $metadata, $markdown, false);
|
||||
}
|
||||
$summary['page_assets_copied'] += copyPageAssets(dirname($contentPath), BLOG_ROOT . '/bl-content/pages/' . $slug, $dryRun);
|
||||
$summary['content_imported']++;
|
||||
|
||||
printf(
|
||||
"%s %-10s %-10s %s -> %s\n",
|
||||
$dryRun ? 'Would import' : 'Imported',
|
||||
$metadata['type'],
|
||||
$metadata['status'],
|
||||
$oldKey,
|
||||
$slug
|
||||
);
|
||||
}
|
||||
|
||||
if (!$skipUploads && is_dir($paths['uploads'])) {
|
||||
$summary['uploads_copied'] = copyTree($paths['uploads'], BLOG_ROOT . '/bl-content/uploads', $dryRun);
|
||||
line(($dryRun ? 'Would copy' : 'Copied') . ' uploads from ' . $paths['uploads']);
|
||||
}
|
||||
|
||||
if (!$skipComments) {
|
||||
$commentFiles = [];
|
||||
if (isset($opts['comments'])) {
|
||||
$commentFiles[] = (string) $opts['comments'];
|
||||
}
|
||||
$commentFiles = array_merge($commentFiles, discoverCommentFiles($paths['content']));
|
||||
$seenCommentFiles = [];
|
||||
foreach ($commentFiles as $commentFile) {
|
||||
$real = realpath($commentFile);
|
||||
if ($real === false || isset($seenCommentFiles[$real])) {
|
||||
continue;
|
||||
}
|
||||
$seenCommentFiles[$real] = true;
|
||||
$records = readComments($real);
|
||||
foreach ($records as $record) {
|
||||
$oldSlug = (string) ($record['slug'] ?? '');
|
||||
$slug = $slugMap[$oldSlug] ?? $slugMap[str_replace('-', '/', $oldSlug)] ?? ContentRepository::slugify($oldSlug);
|
||||
$body = trim((string) ($record['body'] ?? $record['content'] ?? $record['comment'] ?? ''));
|
||||
if ($slug === '' || $body === '') {
|
||||
continue;
|
||||
}
|
||||
if (!$dryRun) {
|
||||
$comments->import($slug, [
|
||||
'author' => $record['author'] ?? $record['name'] ?? 'Imported',
|
||||
'email' => $record['email'] ?? '',
|
||||
'website' => $record['website'] ?? $record['url'] ?? '',
|
||||
'body' => $body,
|
||||
'status' => $record['status'] ?? 'approved',
|
||||
'created_at' => normalizeDate((string) ($record['date'] ?? $record['created_at'] ?? ''), $timezone),
|
||||
'updated_at' => normalizeDate((string) ($record['modified'] ?? $record['updated_at'] ?? $record['date'] ?? ''), $timezone),
|
||||
'user_agent' => $record['user_agent'] ?? $record['agent'] ?? 'bludit-import',
|
||||
]);
|
||||
}
|
||||
$summary['comments_imported']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
$repo->rebuildIndex();
|
||||
}
|
||||
|
||||
line('');
|
||||
line('Import summary:');
|
||||
foreach ($summary as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
continue;
|
||||
}
|
||||
line(' ' . $key . ': ' . (is_bool($value) ? ($value ? 'yes' : 'no') : (string) $value));
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
fail($error->getMessage());
|
||||
}
|
||||
|
||||
function help(): void
|
||||
{
|
||||
echo <<<TEXT
|
||||
Import Bludit content from a local directory on this server.
|
||||
|
||||
Usage:
|
||||
php cli/import-bludit.php /var/www/old-bludit
|
||||
php cli/import-bludit.php /var/www/old-bludit/bl-content
|
||||
php cli/import-bludit.php --source=/var/www/old-bludit --dry-run
|
||||
|
||||
Options:
|
||||
--source=/path Local Bludit root or bl-content directory.
|
||||
--dry-run Show what would import without writing files.
|
||||
--overwrite Reuse matching slugs instead of adding numeric suffixes.
|
||||
--skip-uploads Do not copy bl-content/uploads.
|
||||
--skip-comments Do not scan/import local comment data.
|
||||
--comments=/path/file Extra local JSON/PHP/CSV comment export to import.
|
||||
--source-timezone=Zone Timezone for Bludit database dates without offsets.
|
||||
|
||||
Preserves:
|
||||
Original publish dates, modified dates, static pages, post status, categories,
|
||||
tags, authors, cover image metadata, allow-comments flags, page order, uploads,
|
||||
page-local files, and best-effort local comments.
|
||||
|
||||
TEXT;
|
||||
}
|
||||
|
||||
/** @return array{0: array<int, string>, 1: array<string, mixed>} */
|
||||
function parseArguments(array $argv): array
|
||||
{
|
||||
$args = [];
|
||||
$opts = [];
|
||||
foreach ($argv as $arg) {
|
||||
if (str_starts_with($arg, '--')) {
|
||||
$option = substr($arg, 2);
|
||||
if (str_contains($option, '=')) {
|
||||
[$key, $value] = explode('=', $option, 2);
|
||||
$opts[$key] = $value;
|
||||
} else {
|
||||
$opts[$option] = true;
|
||||
}
|
||||
} elseif (str_starts_with($arg, '-')) {
|
||||
$opts[ltrim($arg, '-')] = true;
|
||||
} else {
|
||||
$args[] = $arg;
|
||||
}
|
||||
}
|
||||
|
||||
return [$args, $opts];
|
||||
}
|
||||
|
||||
/** @return array{root: string, content: string, pages: string, databases: string, uploads: string} */
|
||||
function resolveBluditSource(string $source): array
|
||||
{
|
||||
$path = realpath($source);
|
||||
if ($path === false || !is_dir($path)) {
|
||||
fail('Source directory not found or not readable: ' . $source);
|
||||
}
|
||||
|
||||
if (is_dir($path . '/bl-content')) {
|
||||
$root = $path;
|
||||
$content = $path . '/bl-content';
|
||||
} elseif (basename($path) === 'bl-content' || is_dir($path . '/pages')) {
|
||||
$content = $path;
|
||||
$root = dirname($path);
|
||||
} else {
|
||||
fail('Source must be a Bludit root or a bl-content directory: ' . $source);
|
||||
}
|
||||
|
||||
$pages = $content . '/pages';
|
||||
$databases = $content . '/databases';
|
||||
if (!is_dir($pages)) {
|
||||
fail('Bludit pages directory not found: ' . $pages);
|
||||
}
|
||||
|
||||
return [
|
||||
'root' => $root,
|
||||
'content' => $content,
|
||||
'pages' => $pages,
|
||||
'databases' => is_dir($databases) ? $databases : $content,
|
||||
'uploads' => $content . '/uploads',
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
function readDatabase(string $path): array
|
||||
{
|
||||
if (!is_file($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$raw = (string) file_get_contents($path);
|
||||
$json = trim($raw);
|
||||
$firstObject = strpos($json, '{');
|
||||
$firstArray = strpos($json, '[');
|
||||
$start = false;
|
||||
if ($firstObject !== false && $firstArray !== false) {
|
||||
$start = min($firstObject, $firstArray);
|
||||
} elseif ($firstObject !== false) {
|
||||
$start = $firstObject;
|
||||
} elseif ($firstArray !== false) {
|
||||
$start = $firstArray;
|
||||
}
|
||||
if ($start !== false) {
|
||||
$json = substr($json, $start);
|
||||
}
|
||||
|
||||
$decoded = json_decode($json, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
/** @return array<int, array{key: string, path: string, db: array<string, mixed>}> */
|
||||
function discoverPages(string $pagesRoot, array $pagesDb): array
|
||||
{
|
||||
$entries = [];
|
||||
if ($pagesDb !== []) {
|
||||
foreach ($pagesDb as $key => $row) {
|
||||
$file = $pagesRoot . '/' . $key . '/index.txt';
|
||||
if (is_file($file)) {
|
||||
$entries[] = [
|
||||
'key' => (string) $key,
|
||||
'path' => $file,
|
||||
'db' => is_array($row) ? $row : [],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($entries !== []) {
|
||||
return $entries;
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($pagesRoot, FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
foreach ($iterator as $file) {
|
||||
if (!$file->isFile() || $file->getFilename() !== 'index.txt') {
|
||||
continue;
|
||||
}
|
||||
$directory = dirname($file->getPathname());
|
||||
$key = trim(str_replace($pagesRoot, '', $directory), DIRECTORY_SEPARATOR);
|
||||
$entries[] = [
|
||||
'key' => str_replace(DIRECTORY_SEPARATOR, '/', $key),
|
||||
'path' => $file->getPathname(),
|
||||
'db' => [],
|
||||
];
|
||||
}
|
||||
|
||||
usort($entries, static fn (array $a, array $b): int => strcmp($a['key'], $b['key']));
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $row @param array<string, mixed> $categories @param array<string, mixed> $tags */
|
||||
function metadataFromBludit(string $key, array $row, string $markdown, array $categories, array $tags, string $timezone): array
|
||||
{
|
||||
$bluditType = (string) ($row['type'] ?? 'published');
|
||||
$type = $bluditType === 'static' ? 'page' : 'post';
|
||||
$status = in_array($bluditType, ['published', 'static', 'sticky'], true) ? 'published' : 'draft';
|
||||
$categoryKey = (string) ($row['category'] ?? '');
|
||||
$coverImage = (string) ($row['coverImage'] ?? '');
|
||||
if ($coverImage !== '' && !filter_var($coverImage, FILTER_VALIDATE_URL)) {
|
||||
$uuid = (string) ($row['uuid'] ?? '');
|
||||
$coverImage = $uuid !== ''
|
||||
? '/bl-content/uploads/pages/' . $uuid . '/' . ltrim($coverImage, '/')
|
||||
: '/bl-content/uploads/' . ltrim($coverImage, '/');
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => trim((string) ($row['title'] ?? '')) !== ''
|
||||
? (string) $row['title']
|
||||
: titleFromMarkdownOrKey($markdown, $key),
|
||||
'type' => $type,
|
||||
'status' => $status,
|
||||
'date' => normalizeDate((string) ($row['date'] ?? ''), $timezone),
|
||||
'modified' => normalizeDate((string) ($row['dateModified'] ?? $row['date'] ?? ''), $timezone),
|
||||
'category' => categoryName($categoryKey, $categories),
|
||||
'tags' => tagNames($row['tags'] ?? [], $tags),
|
||||
'summary' => (string) ($row['description'] ?? ''),
|
||||
'author' => (string) ($row['username'] ?? 'Imported'),
|
||||
'cover' => $coverImage,
|
||||
'allow_comments' => (bool) ($row['allowComments'] ?? true),
|
||||
'menu_order' => (int) ($row['position'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
function normalizeDate(string $value, string $timezone): string
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return date('c');
|
||||
}
|
||||
|
||||
try {
|
||||
$zone = new DateTimeZone($timezone);
|
||||
} catch (Throwable) {
|
||||
$zone = new DateTimeZone(date_default_timezone_get());
|
||||
}
|
||||
|
||||
try {
|
||||
$date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $value, $zone)
|
||||
?: DateTimeImmutable::createFromFormat(DateTimeInterface::ATOM, $value)
|
||||
?: new DateTimeImmutable($value, $zone);
|
||||
} catch (Throwable) {
|
||||
$date = new DateTimeImmutable('now', $zone);
|
||||
}
|
||||
|
||||
return $date->format(DateTimeInterface::ATOM);
|
||||
}
|
||||
|
||||
function categoryName(string $key, array $categories): string
|
||||
{
|
||||
if ($key !== '' && isset($categories[$key]) && is_array($categories[$key])) {
|
||||
return (string) ($categories[$key]['name'] ?? $key);
|
||||
}
|
||||
|
||||
return $key !== '' ? ucwords(str_replace('-', ' ', $key)) : 'Notes';
|
||||
}
|
||||
|
||||
function tagNames(mixed $rawTags, array $tagsDb): array
|
||||
{
|
||||
if (is_string($rawTags)) {
|
||||
return ContentRepository::normalizeList($rawTags);
|
||||
}
|
||||
if (!is_array($rawTags)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tags = [];
|
||||
foreach ($rawTags as $key => $value) {
|
||||
if (is_string($value) && $value !== '') {
|
||||
$tags[] = $value;
|
||||
} elseif (is_string($key) && isset($tagsDb[$key]) && is_array($tagsDb[$key])) {
|
||||
$tags[] = (string) ($tagsDb[$key]['name'] ?? $key);
|
||||
} elseif (is_string($key)) {
|
||||
$tags[] = ucwords(str_replace('-', ' ', $key));
|
||||
}
|
||||
}
|
||||
|
||||
return ContentRepository::normalizeList($tags);
|
||||
}
|
||||
|
||||
function titleFromMarkdownOrKey(string $markdown, string $key): string
|
||||
{
|
||||
foreach (explode("\n", $markdown) as $line) {
|
||||
if (preg_match('/^#\s+(.+)$/', trim($line), $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return ucwords(str_replace(['-', '/'], ' ', $key));
|
||||
}
|
||||
|
||||
function slugFromBluditKey(string $key): string
|
||||
{
|
||||
return ContentRepository::slugify(str_replace('/', '-', $key));
|
||||
}
|
||||
|
||||
/** @param array<string, bool> $reserved */
|
||||
function uniqueSlug(ContentRepository $repo, string $baseSlug, array $reserved, bool $overwrite): string
|
||||
{
|
||||
$baseSlug = $baseSlug !== '' ? $baseSlug : 'imported-page';
|
||||
if ($overwrite) {
|
||||
return $baseSlug;
|
||||
}
|
||||
|
||||
$slug = $baseSlug;
|
||||
$i = 2;
|
||||
while (isset($reserved[$slug]) || $repo->find($slug, true) !== null) {
|
||||
$slug = $baseSlug . '-' . $i;
|
||||
$i++;
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
function copyPageAssets(string $sourceDir, string $targetDir, bool $dryRun): int
|
||||
{
|
||||
$count = 0;
|
||||
foreach (glob($sourceDir . '/*') ?: [] as $path) {
|
||||
if (basename($path) === 'index.txt') {
|
||||
continue;
|
||||
}
|
||||
$count += copyTree($path, $targetDir . '/' . basename($path), $dryRun);
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
function copyTree(string $source, string $target, bool $dryRun): int
|
||||
{
|
||||
if (!file_exists($source)) {
|
||||
return 0;
|
||||
}
|
||||
$sourceReal = realpath($source);
|
||||
$targetReal = realpath($target);
|
||||
if ($sourceReal !== false && $targetReal !== false && $sourceReal === $targetReal) {
|
||||
return 0;
|
||||
}
|
||||
if ($dryRun) {
|
||||
if (is_file($source) || is_link($source)) {
|
||||
return 1;
|
||||
}
|
||||
$count = 0;
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() || $file->isLink()) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
if (is_link($source)) {
|
||||
$real = realpath($source);
|
||||
return $real === false ? 0 : copyTree($real, $target, false);
|
||||
}
|
||||
if (is_file($source)) {
|
||||
if (!is_dir(dirname($target))) {
|
||||
mkdir(dirname($target), 0775, true);
|
||||
}
|
||||
copy($source, $target);
|
||||
touch($target, filemtime($source) ?: time());
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!is_dir($target)) {
|
||||
mkdir($target, 0775, true);
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
foreach ($iterator as $item) {
|
||||
$relative = substr($item->getPathname(), strlen($source) + 1);
|
||||
$destination = $target . '/' . $relative;
|
||||
if ($item->isDir() && !$item->isLink()) {
|
||||
if (!is_dir($destination)) {
|
||||
mkdir($destination, 0775, true);
|
||||
}
|
||||
} else {
|
||||
$count += copyTree($item->getPathname(), $destination, false);
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/** @return array<int, string> */
|
||||
function discoverCommentFiles(string $contentRoot): array
|
||||
{
|
||||
$files = [];
|
||||
foreach ([$contentRoot . '/databases', $contentRoot . '/workspaces'] as $root) {
|
||||
if (!is_dir($root)) {
|
||||
continue;
|
||||
}
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
foreach ($iterator as $file) {
|
||||
if (!$file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
$name = strtolower($file->getFilename());
|
||||
$extension = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
||||
if (str_contains($name, 'comment') && in_array($extension, ['php', 'json', 'csv', 'sqlite', 'db'], true)) {
|
||||
$files[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/** @return array<int, array<string, mixed>> */
|
||||
function readComments(string $path): array
|
||||
{
|
||||
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
if ($extension === 'csv') {
|
||||
return readCommentCsv($path);
|
||||
}
|
||||
if (in_array($extension, ['sqlite', 'db'], true)) {
|
||||
return readCommentSqlite($path);
|
||||
}
|
||||
|
||||
return collectComments(readDatabase($path));
|
||||
}
|
||||
|
||||
/** @return array<int, array<string, mixed>> */
|
||||
function readCommentCsv(string $path): array
|
||||
{
|
||||
$handle = fopen($path, 'rb');
|
||||
if (!$handle) {
|
||||
return [];
|
||||
}
|
||||
$header = fgetcsv($handle, null, ',', '"', '') ?: [];
|
||||
$records = [];
|
||||
while (($row = fgetcsv($handle, null, ',', '"', '')) !== false) {
|
||||
$record = array_combine($header, $row);
|
||||
if (is_array($record)) {
|
||||
$records[] = normalizeCommentRecord($record);
|
||||
}
|
||||
}
|
||||
fclose($handle);
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
/** @return array<int, array<string, mixed>> */
|
||||
function readCommentSqlite(string $path): array
|
||||
{
|
||||
if (!class_exists(PDO::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = new PDO('sqlite:' . $path);
|
||||
$tables = $pdo->query("SELECT name FROM sqlite_master WHERE type='table'")->fetchAll(PDO::FETCH_COLUMN);
|
||||
$records = [];
|
||||
foreach ($tables as $table) {
|
||||
if (!str_contains(strtolower((string) $table), 'comment')) {
|
||||
continue;
|
||||
}
|
||||
$rows = $pdo->query('SELECT * FROM "' . str_replace('"', '""', (string) $table) . '"')->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($rows as $row) {
|
||||
$records[] = normalizeCommentRecord($row);
|
||||
}
|
||||
}
|
||||
return $records;
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<int, array<string, mixed>> */
|
||||
function collectComments(mixed $value, string $slugHint = ''): array
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
if (isCommentRecord($value)) {
|
||||
$record = normalizeCommentRecord($value);
|
||||
if (($record['slug'] ?? '') === '' && $slugHint !== '') {
|
||||
$record['slug'] = $slugHint;
|
||||
}
|
||||
return [$record];
|
||||
}
|
||||
|
||||
$records = [];
|
||||
foreach ($value as $key => $child) {
|
||||
$nextHint = is_string($key) && $key !== '' && !is_numeric($key) ? $key : $slugHint;
|
||||
foreach (collectComments($child, $nextHint) as $record) {
|
||||
$records[] = $record;
|
||||
}
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
function isCommentRecord(array $value): bool
|
||||
{
|
||||
$bodyKeys = ['body', 'content', 'comment', 'message', 'text'];
|
||||
$identityKeys = ['author', 'name', 'email', 'username'];
|
||||
return array_intersect($bodyKeys, array_keys($value)) !== []
|
||||
&& array_intersect($identityKeys, array_keys($value)) !== [];
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $record @return array<string, mixed> */
|
||||
function normalizeCommentRecord(array $record): array
|
||||
{
|
||||
$slug = (string) (
|
||||
$record['slug']
|
||||
?? $record['page']
|
||||
?? $record['pageKey']
|
||||
?? $record['page_key']
|
||||
?? $record['post']
|
||||
?? $record['post_slug']
|
||||
?? ''
|
||||
);
|
||||
|
||||
return [
|
||||
'slug' => $slug,
|
||||
'author' => $record['author'] ?? $record['name'] ?? $record['username'] ?? 'Imported',
|
||||
'email' => $record['email'] ?? '',
|
||||
'website' => $record['website'] ?? $record['url'] ?? $record['site'] ?? '',
|
||||
'body' => $record['body'] ?? $record['content'] ?? $record['comment'] ?? $record['message'] ?? $record['text'] ?? '',
|
||||
'status' => $record['status'] ?? $record['state'] ?? 'approved',
|
||||
'date' => $record['date'] ?? $record['created'] ?? $record['created_at'] ?? '',
|
||||
'modified' => $record['modified'] ?? $record['updated'] ?? $record['updated_at'] ?? '',
|
||||
'user_agent' => $record['user_agent'] ?? $record['agent'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
function line(string $message): void
|
||||
{
|
||||
echo $message . PHP_EOL;
|
||||
}
|
||||
|
||||
function fail(string $message): never
|
||||
{
|
||||
fwrite(STDERR, $message . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user