647 lines
21 KiB
PHP
Executable File
647 lines
21 KiB
PHP
Executable File
#!/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);
|
|
}
|