Add BluditGithubImport class for GitHub article import functionality
- Implemented methods for slug validation, path validation, scalar parsing, and document processing. - Added link and image handling with safety checks for content. - Introduced blob verification for GitHub responses and asset type validation. - Created unit tests for the import functionality, covering various edge cases and ensuring robust error handling. - Included tests for metadata preservation, asset handling, and concurrent edits.
This commit is contained in:
1 parent
feefb08a85
commit
3dfd367814
6 files changed
+848
-80
No files matched your search
+339
-27
@@ -1,7 +1,10 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . "/importer.php";
|
||||
|
||||
class pluginBluditGithub extends Plugin
|
||||
{
|
||||
private $importing = false;
|
||||
public function init()
|
||||
{
|
||||
$this->dbFields = [
|
||||
@@ -69,6 +72,8 @@ class pluginBluditGithub extends Plugin
|
||||
$html .= '<input name="bulkExportPublished" type="submit" class="btn btn-secondary" value="Export published only" style="margin-left:0.5em">';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= $this->importForm();
|
||||
|
||||
if ($status) {
|
||||
$isError = stripos($status, 'error') !== false;
|
||||
$alertClass = $isError ? 'alert-danger' : 'alert-success';
|
||||
@@ -82,17 +87,31 @@ class pluginBluditGithub extends Plugin
|
||||
{
|
||||
parent::post();
|
||||
|
||||
if (!empty($_POST['bulkExportAll'])) {
|
||||
$this->bulkExport(true);
|
||||
} elseif (!empty($_POST['bulkExportPublished'])) {
|
||||
$this->bulkExport(false);
|
||||
try {
|
||||
if (!empty($_POST['previewImport'])) {
|
||||
$this->previewImport($_POST['importSlug'] ?? '', !empty($_POST['importAsDraft']));
|
||||
} elseif (!empty($_POST['confirmImport'])) {
|
||||
$this->confirmImport($_POST['importConfirmation'] ?? '');
|
||||
} elseif (!empty($_POST['cancelImport'])) {
|
||||
Session::remove('bluditGithubImport');
|
||||
$this->setStatus('Import preview cancelled.');
|
||||
} else {
|
||||
Session::remove('bluditGithubImport');
|
||||
if (!empty($_POST['bulkExportAll'])) {
|
||||
$this->bulkExport(true);
|
||||
} elseif (!empty($_POST['bulkExportPublished'])) {
|
||||
$this->bulkExport(false);
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->setStatus('Error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function afterPageCreate($key)
|
||||
{
|
||||
try {
|
||||
if (empty($key) || !$this->isConfigured()) {
|
||||
if ($this->importing || empty($key) || !$this->isConfigured()) {
|
||||
return;
|
||||
}
|
||||
$page = new Page($key);
|
||||
@@ -110,7 +129,7 @@ class pluginBluditGithub extends Plugin
|
||||
public function afterPageModify($key)
|
||||
{
|
||||
try {
|
||||
if (empty($key) || !$this->isConfigured()) {
|
||||
if ($this->importing || empty($key) || !$this->isConfigured()) {
|
||||
return;
|
||||
}
|
||||
$page = new Page($key);
|
||||
@@ -128,7 +147,7 @@ class pluginBluditGithub extends Plugin
|
||||
public function afterPageDelete($key)
|
||||
{
|
||||
try {
|
||||
if (empty($key) || !$this->isConfigured()) {
|
||||
if ($this->importing || empty($key) || !$this->isConfigured()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -146,13 +165,14 @@ class pluginBluditGithub extends Plugin
|
||||
]);
|
||||
}
|
||||
|
||||
// Delete all files in img/
|
||||
$items = $this->githubRequest('GET', '/contents/' . $base . '/img');
|
||||
if (is_array($items)) {
|
||||
// Delete article image and attachment files (one directory level, like the exporter).
|
||||
foreach (['img', 'files'] as $directory) {
|
||||
$items = $this->githubRequest('GET', '/contents/' . $base . '/' . $directory);
|
||||
if (!is_array($items)) { continue; }
|
||||
foreach ($items as $item) {
|
||||
if ($item['type'] === 'file') {
|
||||
if (($item['type'] ?? '') === 'file') {
|
||||
$this->githubRequest('DELETE', '/contents/' . $item['path'], [
|
||||
'message' => 'Delete image: ' . $key . '/img/' . $item['name'],
|
||||
'message' => 'Delete asset: ' . $key . '/' . $directory . '/' . $item['name'],
|
||||
'sha' => $item['sha'],
|
||||
'branch' => $this->getValue('branch'),
|
||||
]);
|
||||
@@ -164,6 +184,278 @@ class pluginBluditGithub extends Plugin
|
||||
}
|
||||
}
|
||||
|
||||
// Import previews are bound to the current admin session and expire after 30 minutes.
|
||||
private function importForm()
|
||||
{
|
||||
$plan = Session::get('bluditGithubImport');
|
||||
$e = function ($v) { return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); };
|
||||
$draftChecked = !is_array($plan) || !empty($plan['asDraft']) ? ' checked' : '';
|
||||
$html = '<div style="margin-top:1.5em;padding-top:1em;border-top:1px solid #eee">';
|
||||
$html .= '<label>Import one article from GitHub</label>';
|
||||
$html .= '<input name="importSlug" type="text" value="' . $e($plan['slug'] ?? '') . '" placeholder="load-balancing-my-homelab">';
|
||||
$html .= '<label><input type="checkbox" name="importAsDraft" value="1"' . $draftChecked . '> Import as draft (including existing articles)</label>';
|
||||
$html .= '<p class="tip">Preview reads the configured branch without changing articles. Confirmation replaces the selected page only. No automatic pulling or deletion.</p>';
|
||||
$html .= '<button name="previewImport" value="1" type="submit" class="btn btn-secondary">Preview import</button>';
|
||||
if (is_array($plan)) {
|
||||
$html .= '<hr><h4>Import preview: ' . $e($plan['slug']) . '</h4>';
|
||||
$html .= '<p>' . ($plan['local'] === null ? 'Create a new page' : 'Replace the existing page') . ' from commit <code>' . $e($plan['commit']) . '</code>.</p>';
|
||||
$html .= '<dl>';
|
||||
foreach (['title', 'description', 'date', 'category', 'tags', 'type'] as $field) {
|
||||
$value = $plan['args'][$field] ?? '(preserve existing value)';
|
||||
if ($field === 'type') { $value = $plan['effectiveType']; }
|
||||
$html .= '<dt>' . $e($field === 'type' ? 'Resulting status' : $field) . '</dt><dd>' . $e($value) . '</dd>';
|
||||
}
|
||||
$html .= '</dl><p>Assets to download:</p><ul>';
|
||||
foreach ($plan['assets'] as $asset) {
|
||||
$html .= '<li><code>' . $e($asset['relative']) . '</code> (' . (int)$asset['size'] . ' bytes)</li>';
|
||||
}
|
||||
$html .= '</ul>';
|
||||
if ($plan['local'] !== null) {
|
||||
$html .= '<details><summary>Current local content</summary><textarea readonly rows="10" style="width:100%">' . $e($plan['local']['content']) . '</textarea></details>';
|
||||
}
|
||||
$html .= '<label>Incoming content (escaped; not executed)</label>';
|
||||
$html .= '<textarea readonly rows="15" style="width:100%">' . $e($plan['content']) . '</textarea>';
|
||||
$html .= '<input type="hidden" name="importConfirmation" value="' . $e($plan['confirmation']) . '">';
|
||||
$html .= '<p>A protected local backup is saved before replacing an existing page. Other page metadata is preserved.</p>';
|
||||
$html .= '<button name="confirmImport" value="1" type="submit" class="btn btn-primary">Confirm this import</button> ';
|
||||
$html .= '<button name="cancelImport" value="1" type="submit" class="btn btn-secondary">Cancel preview</button>';
|
||||
}
|
||||
return $html . '</div>';
|
||||
}
|
||||
|
||||
private function importConfigHash()
|
||||
{
|
||||
$values = [];
|
||||
foreach (['owner', 'repo', 'branch', 'path', 'token'] as $key) { $values[] = $this->getValue($key); }
|
||||
return hash('sha256', json_encode($values));
|
||||
}
|
||||
|
||||
private function importLocalState($slug)
|
||||
{
|
||||
$current = new Pages();
|
||||
if (!$current->exists($slug)) { return null; }
|
||||
$content = file_get_contents(PATH_PAGES . $slug . DS . FILENAME);
|
||||
if ($content === false) { throw new RuntimeException('Cannot read the existing page content.'); }
|
||||
return ['row' => $current->getPageDB($slug), 'content' => $content];
|
||||
}
|
||||
|
||||
private function previewImport($slug, $asDraft)
|
||||
{
|
||||
global $pages, $categories;
|
||||
Session::remove('bluditGithubImport');
|
||||
if (!$this->isConfigured()) { throw new RuntimeException('Configure the GitHub repository first.'); }
|
||||
BluditGithubImport::slug($slug);
|
||||
// Refuse Bludit slug normalization/collisions instead of importing into a different page.
|
||||
if ($pages->generateKey($slug, false, false, $pages->exists($slug) ? $slug : '') !== $slug) {
|
||||
throw new RuntimeException('Bludit would change this slug. Choose an exact, supported page key.');
|
||||
}
|
||||
$prefix = trim($this->getValue('path'), '/');
|
||||
$base = ($prefix === '' ? '' : BluditGithubImport::path($prefix) . '/') . $slug . '/';
|
||||
$commit = $this->githubRequest('GET', '/commits/' . rawurlencode($this->getValue('branch')));
|
||||
if (!is_array($commit) || !preg_match('/\A[a-f0-9]{40}\z/', $commit['sha'] ?? '') || empty($commit['commit']['tree']['sha'])) {
|
||||
throw new RuntimeException('Cannot resolve the configured GitHub branch. Check the repository, token and branch.');
|
||||
}
|
||||
$tree = $this->githubRequest('GET', '/git/trees/' . rawurlencode($commit['commit']['tree']['sha']) . '?recursive=1');
|
||||
if (!is_array($tree) || !isset($tree['tree']) || !empty($tree['truncated'])) {
|
||||
throw new RuntimeException('Cannot read a complete repository tree.');
|
||||
}
|
||||
$entries = [];
|
||||
foreach ($tree['tree'] as $item) { $entries[$item['path']] = $item; }
|
||||
$document = $entries[$base . 'content/index.md'] ?? null;
|
||||
if (!$document) { throw new RuntimeException('Article content/index.md not found on the configured branch.'); }
|
||||
$raw = $this->importBlob($document, 1024 * 1024);
|
||||
$parsed = BluditGithubImport::document($raw, $slug);
|
||||
$meta = $parsed['meta'];
|
||||
$local = $this->importLocalState($slug);
|
||||
$uuid = $local === null ? bin2hex(random_bytes(16)) : $local['row']['uuid'];
|
||||
if (!is_string($uuid) || !preg_match('/\A[a-zA-Z0-9_-]+\z/', $uuid)) {
|
||||
throw new RuntimeException('Invalid Bludit page upload identifier.');
|
||||
}
|
||||
$args = ['slug' => $slug, 'title' => Sanitize::htmlDecode($meta['title'])];
|
||||
foreach (['description', 'category'] as $field) {
|
||||
if (array_key_exists($field, $meta)) { $args[$field] = Sanitize::htmlDecode($meta[$field]); }
|
||||
}
|
||||
if (!empty($args['category']) && !$categories->exists($args['category'])) {
|
||||
throw new RuntimeException('Create the category in Bludit before importing: ' . $args['category']);
|
||||
}
|
||||
if (isset($meta['tags'])) {
|
||||
foreach ($meta['tags'] as $tag) {
|
||||
if (strpos($tag, ',') !== false) { throw new RuntimeException('Tags cannot contain commas.'); }
|
||||
}
|
||||
$args['tags'] = implode(',', $meta['tags']);
|
||||
}
|
||||
if (!empty($meta['date'])) {
|
||||
$oldDate = $local['row']['date'] ?? '';
|
||||
$args['date'] = substr($oldDate, 0, 10) === $meta['date'] ? $oldDate : $meta['date'] . ' 00:00:00';
|
||||
}
|
||||
$args['type'] = $asDraft ? 'draft' : (($meta['status'] ?? '') ?: ($local['row']['type'] ?? 'draft'));
|
||||
$effectiveType = $args['type'];
|
||||
if ($effectiveType === 'published' && ($args['date'] ?? ($local['row']['date'] ?? '')) > date('Y-m-d H:i:s')) {
|
||||
$effectiveType = 'scheduled';
|
||||
}
|
||||
$links = BluditGithubImport::links($parsed['content']);
|
||||
if (!empty($meta['cover'])) {
|
||||
if (strpos($meta['cover'], '../') === 0) {
|
||||
$coverLinks = BluditGithubImport::links('<img src="' . $meta['cover'] . '">');
|
||||
$links += $coverLinks;
|
||||
} elseif (!preg_match('~\Ahttps?://[^\s<>"\x27]+\z~', $meta['cover'])) {
|
||||
throw new RuntimeException('Cover must be ../img/filename or an HTTP(S) URL.');
|
||||
}
|
||||
}
|
||||
$assets = [];
|
||||
$total = 0;
|
||||
foreach ($links as $url => $relative) {
|
||||
$entry = $entries[$base . $relative] ?? null;
|
||||
if (!$entry || ($entry['type'] ?? '') !== 'blob' || !in_array($entry['mode'] ?? '', ['100644', '100755'], true)) {
|
||||
throw new RuntimeException('Missing asset or unsupported symlink: ' . $relative);
|
||||
}
|
||||
$ext = BluditGithubImport::extension($relative);
|
||||
$total += (int)($entry['size'] ?? 0);
|
||||
if (($entry['size'] ?? 0) > 10 * 1024 * 1024 || $total > 32 * 1024 * 1024 || count($assets) >= 100) {
|
||||
throw new RuntimeException('Import exceeds the asset limits (100 files, 10 MiB/file, 32 MiB total).');
|
||||
}
|
||||
$assets[$url] = ['relative' => $relative, 'sha' => $entry['sha'], 'mode' => $entry['mode'], 'type' => 'blob', 'size' => $entry['size'],
|
||||
'filename' => 'gh-' . $entry['sha'] . '-' . substr(sha1($relative), 0, 12) . '.' . $ext];
|
||||
}
|
||||
$plan = ['slug' => $slug, 'commit' => $commit['sha'], 'config' => $this->importConfigHash(), 'created' => time(),
|
||||
'confirmation' => bin2hex(random_bytes(24)), 'local' => $local, 'uuid' => $uuid, 'args' => $args,
|
||||
'effectiveType' => $effectiveType, 'asDraft' => $asDraft, 'content' => $parsed['content'], 'cover' => $meta['cover'] ?? null, 'assets' => $assets];
|
||||
Session::set('bluditGithubImport', $plan);
|
||||
$this->setStatus('Preview ready. Review the incoming content and resulting status, then confirm to import.');
|
||||
}
|
||||
|
||||
private function importBlob($entry, $limit)
|
||||
{
|
||||
if (($entry['type'] ?? '') !== 'blob' || !in_array($entry['mode'] ?? '', ['100644', '100755'], true) || !preg_match('/\A[a-f0-9]{40}\z/', $entry['sha'] ?? '') || ($entry['size'] ?? 0) > $limit) {
|
||||
throw new RuntimeException('Invalid Git blob or file too large.');
|
||||
}
|
||||
return BluditGithubImport::blob($this->githubRequest('GET', '/git/blobs/' . $entry['sha']), $entry['sha'], $limit);
|
||||
}
|
||||
|
||||
private function writeProtectedJson($file, $value)
|
||||
{
|
||||
$json = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
$tmp = tempnam($this->workspace(), '.github-');
|
||||
if ($tmp === false) { throw new RuntimeException('Cannot create a protected plugin workspace file.'); }
|
||||
try {
|
||||
if (!chmod($tmp, 0600) || file_put_contents($tmp, "<?php http_response_code(403); exit; ?>\n" . $json) === false || !rename($tmp, $file)) {
|
||||
throw new RuntimeException('Cannot save protected plugin workspace data.');
|
||||
}
|
||||
} finally { if (file_exists($tmp)) { unlink($tmp); } }
|
||||
}
|
||||
|
||||
private function assetMap($slug)
|
||||
{
|
||||
$file = $this->workspace() . 'assets-' . hash('sha256', $slug) . '.php';
|
||||
if (!is_file($file)) { return []; }
|
||||
$raw = file_get_contents($file);
|
||||
$data = json_decode(substr($raw, strpos($raw, "\n") + 1), true);
|
||||
if (!is_array($data)) { throw new RuntimeException('Cannot read imported asset mapping.'); }
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function confirmImport($confirmation)
|
||||
{
|
||||
global $pages;
|
||||
$plan = Session::get('bluditGithubImport');
|
||||
if (!is_array($plan) || !is_string($confirmation) || !hash_equals($plan['confirmation'], $confirmation) || time() - $plan['created'] > 1800 || !hash_equals($plan['config'], $this->importConfigHash())) {
|
||||
Session::remove('bluditGithubImport');
|
||||
throw new RuntimeException('Import preview expired or settings changed. Preview again.');
|
||||
}
|
||||
$lock = fopen($this->workspace() . 'import.lock', 'c');
|
||||
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) {
|
||||
if ($lock) { fclose($lock); }
|
||||
throw new RuntimeException('Another import is running. Retry later.');
|
||||
}
|
||||
$installed = [];
|
||||
$attempted = false;
|
||||
$backup = null;
|
||||
try {
|
||||
if ($this->importLocalState($plan['slug']) !== $plan['local']) {
|
||||
throw new RuntimeException('The local page changed since preview. Preview again before replacing it.');
|
||||
}
|
||||
$files = [];
|
||||
foreach ($plan['assets'] as $asset) {
|
||||
$data = $this->importBlob($asset, 10 * 1024 * 1024);
|
||||
$ext = BluditGithubImport::extension($asset['relative']);
|
||||
if (in_array($ext, ['png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'ico'], true)) {
|
||||
$image = @getimagesizefromstring($data);
|
||||
$types = ['png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', 'webp' => 'image/webp', 'avif' => 'image/avif', 'ico' => 'image/vnd.microsoft.icon'];
|
||||
if (!$image || ($image['mime'] ?? '') !== $types[$ext]) { throw new RuntimeException('Invalid image contents: ' . $asset['relative']); }
|
||||
}
|
||||
$files[$asset['filename']] = $data;
|
||||
}
|
||||
// Downloads may take time; catch edits made while waiting for GitHub.
|
||||
if ($this->importLocalState($plan['slug']) !== $plan['local']) {
|
||||
throw new RuntimeException('The local page changed during asset download. Preview again.');
|
||||
}
|
||||
if ($plan['local'] !== null) {
|
||||
$backup = $this->workspace() . 'backup-' . date('Ymd-His') . '-' . bin2hex(random_bytes(8)) . '.php';
|
||||
$this->writeProtectedJson($backup, ['slug' => $plan['slug'], 'commit' => $plan['commit'], 'page' => $plan['local'], 'assetMap' => $this->assetMap($plan['slug'])]);
|
||||
}
|
||||
$dir = PATH_UPLOADS_PAGES . $plan['uuid'] . DS;
|
||||
if (is_link(rtrim($dir, DS)) || (!is_dir($dir) && !mkdir($dir, 0755, true))) { throw new RuntimeException('Cannot prepare the page upload directory.'); }
|
||||
foreach ($files as $name => $data) {
|
||||
$dest = $dir . $name;
|
||||
if (is_link($dest)) { throw new RuntimeException('Refusing a symlink in page uploads.'); }
|
||||
if (is_file($dest)) {
|
||||
if (file_get_contents($dest) !== $data) { throw new RuntimeException('An imported asset filename already has different contents.'); }
|
||||
continue;
|
||||
}
|
||||
$tmp = tempnam($dir, '.github-');
|
||||
if ($tmp === false) { throw new RuntimeException('Cannot stage an imported asset.'); }
|
||||
try {
|
||||
if (file_put_contents($tmp, $data) === false || !chmod($tmp, 0644) || !rename($tmp, $dest)) { throw new RuntimeException('Cannot install an imported asset.'); }
|
||||
} finally { if (is_file($tmp)) { unlink($tmp); } }
|
||||
$installed[] = $dest;
|
||||
}
|
||||
$url = rtrim(DOMAIN_UPLOADS_PAGES, '/') . '/' . $plan['uuid'] . '/';
|
||||
$args = $plan['args'];
|
||||
$args['content'] = BluditGithubImport::links($plan['content'], function ($relative) use ($plan, $url) {
|
||||
return $url . $plan['assets'][$relative]['filename'];
|
||||
});
|
||||
if ($plan['cover'] !== null) {
|
||||
$args['coverImage'] = isset($plan['assets'][$plan['cover']]) ? $url . $plan['assets'][$plan['cover']]['filename'] : $plan['cover'];
|
||||
}
|
||||
if ($plan['local'] === null) { $args['uuid'] = $plan['uuid']; } else { $args['key'] = $plan['slug']; }
|
||||
if ($this->importLocalState($plan['slug']) !== $plan['local']) {
|
||||
throw new RuntimeException('The local page changed before save. Preview again.');
|
||||
}
|
||||
$pages = new Pages();
|
||||
$this->importing = true;
|
||||
$attempted = true;
|
||||
$key = $plan['local'] === null ? createPage($args) : editPage($args);
|
||||
if ($key !== $plan['slug'] || !$pages->save()) { throw new RuntimeException('Bludit could not persist the imported page.'); }
|
||||
$map = $this->assetMap($plan['slug']);
|
||||
foreach ($plan['assets'] as $asset) { $map[$asset['filename']] = $asset['relative']; }
|
||||
$this->writeProtectedJson($this->workspace() . 'assets-' . hash('sha256', $plan['slug']) . '.php', $map);
|
||||
Session::remove('bluditGithubImport');
|
||||
$this->setStatus('Imported ' . $key . ' from ' . substr($plan['commit'], 0, 12) . ' as ' . (new Page($key))->getValue('type') . '. No export was triggered.' . ($backup ? ' Backup: ' . basename($backup) : ''));
|
||||
} catch (Throwable $e) {
|
||||
if ($attempted) {
|
||||
try {
|
||||
if ($plan['local'] === null) {
|
||||
if ($pages->exists($plan['slug']) && !$pages->delete($plan['slug'])) { throw new RuntimeException('Cannot remove partial new page.'); }
|
||||
} else {
|
||||
if (file_put_contents(PATH_PAGES . $plan['slug'] . DS . FILENAME, $plan['local']['content']) === false) { throw new RuntimeException('Cannot restore page content.'); }
|
||||
$pages->db[$plan['slug']] = $plan['local']['row'];
|
||||
if (!$pages->save()) { throw new RuntimeException('Cannot restore page metadata.'); }
|
||||
}
|
||||
reindexCategories();
|
||||
reindexTags();
|
||||
} catch (Throwable $restore) {
|
||||
throw new RuntimeException('Import failed and automatic restoration failed. Restore the protected backup ' . ($backup ? basename($backup) : '(new page)') . ': ' . $restore->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
foreach ($installed as $file) { if (is_file($file)) { unlink($file); } }
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->importing = false;
|
||||
flock($lock, LOCK_UN);
|
||||
fclose($lock);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Private ---
|
||||
|
||||
|
||||
@@ -173,7 +465,7 @@ class pluginBluditGithub extends Plugin
|
||||
$rawContent = $page->content();
|
||||
|
||||
// Export images using URLs found in the content (before rewriting)
|
||||
$this->exportImages($key, $rawContent);
|
||||
$this->exportImages($key, $rawContent . "\n" . $page->coverImage());
|
||||
|
||||
$content = $this->buildMarkdown($page, $key, $rawContent);
|
||||
$filePath = $this->getFilePath($key);
|
||||
@@ -195,6 +487,12 @@ class pluginBluditGithub extends Plugin
|
||||
}
|
||||
}
|
||||
|
||||
private function defaultAssetPath($filename)
|
||||
{
|
||||
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
return (in_array($ext, ['txt', 'pdf', 'csv', 'json', 'zip'], true) ? 'files/' : 'img/') . $filename;
|
||||
}
|
||||
|
||||
private function exportImages($key, $content)
|
||||
{
|
||||
preg_match_all(
|
||||
@@ -209,6 +507,7 @@ class pluginBluditGithub extends Plugin
|
||||
}
|
||||
|
||||
$prefix = trim($this->getValue('path'), '/');
|
||||
$map = $this->assetMap($key);
|
||||
$seen = [];
|
||||
|
||||
foreach ($matches as $match) {
|
||||
@@ -226,11 +525,13 @@ class pluginBluditGithub extends Plugin
|
||||
continue;
|
||||
}
|
||||
|
||||
$repoPath = ($prefix ? $prefix . '/' : '') . $key . '/img/' . $filename;
|
||||
$relative = $map[$filename] ?? $this->defaultAssetPath($filename);
|
||||
BluditGithubImport::path($relative);
|
||||
$repoPath = ($prefix ? $prefix . '/' : '') . $key . '/' . $relative;
|
||||
$sha = $this->getFileSha($repoPath);
|
||||
|
||||
$data = [
|
||||
'message' => ($sha ? 'Update' : 'Add') . ' image: ' . $key . '/img/' . $filename,
|
||||
'message' => ($sha ? 'Update' : 'Add') . ' asset: ' . $key . '/' . $relative,
|
||||
'content' => base64_encode($imageData),
|
||||
'branch' => $this->getValue('branch'),
|
||||
];
|
||||
@@ -302,9 +603,12 @@ class pluginBluditGithub extends Plugin
|
||||
|
||||
// Rewrite absolute Bludit upload URLs to relative img/ paths
|
||||
// e.g. https://domain/bl-content/uploads/pages/UUID/image.png → ../img/image.png
|
||||
$content = preg_replace(
|
||||
$map = $this->assetMap($key);
|
||||
$content = preg_replace_callback(
|
||||
'/https?:\/\/[^\/]+\/bl-content\/uploads\/pages\/[^\/]+\/([^"\')\s<>]+)/',
|
||||
'../img/$1',
|
||||
function ($match) use ($map) {
|
||||
return '../' . ($map[$match[1]] ?? $this->defaultAssetPath($match[1]));
|
||||
},
|
||||
$content
|
||||
);
|
||||
|
||||
@@ -341,31 +645,36 @@ class pluginBluditGithub extends Plugin
|
||||
foreach ($tagArray as $tag) {
|
||||
$tag = trim((string)$tag);
|
||||
if ($tag !== '') {
|
||||
$tagLines .= "\n - " . $tag;
|
||||
$tagLines .= "\n - " . json_encode($tag, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$fm = "---\n";
|
||||
$fm .= 'title: "' . str_replace('"', '\"', $page->title()) . "\"\n";
|
||||
$fm .= 'title: ' . json_encode(Sanitize::htmlDecode($page->title()), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
$fm .= 'date: ' . date('Y-m-d', strtotime($page->getValue('dateRaw'))) . "\n";
|
||||
$fm .= 'slug: ' . $key . "\n";
|
||||
$fm .= 'status: ' . $page->getValue('status') . "\n";
|
||||
$fm .= 'status: ' . $page->getValue('type') . "\n";
|
||||
$fm .= 'tags:' . ($tagLines ?: ' []') . "\n";
|
||||
|
||||
$description = $page->getValue('description');
|
||||
$description = Sanitize::htmlDecode($page->getValue('description'));
|
||||
if (!empty($description)) {
|
||||
$fm .= 'description: "' . str_replace('"', '\"', $description) . "\"\n";
|
||||
$fm .= 'description: ' . json_encode($description, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
}
|
||||
|
||||
$category = $page->getValue('category');
|
||||
$category = Sanitize::htmlDecode($page->getValue('category'));
|
||||
if (!empty($category) && $category !== 'uncategorized') {
|
||||
$fm .= 'category: ' . $category . "\n";
|
||||
$fm .= 'category: ' . json_encode($category, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
}
|
||||
|
||||
$cover = $page->coverImage();
|
||||
if (!empty($cover)) {
|
||||
$fm .= 'cover: ' . $cover . "\n";
|
||||
$cover = preg_replace_callback(
|
||||
'/https?:\/\/[^\/]+\/bl-content\/uploads\/pages\/[^\/]+\/([^"\')\s<>]+)/',
|
||||
function ($match) use ($map) { return '../' . ($map[$match[1]] ?? $this->defaultAssetPath($match[1])); },
|
||||
$cover
|
||||
);
|
||||
$fm .= 'cover: ' . json_encode($cover, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
}
|
||||
|
||||
$fm .= "---\n\n";
|
||||
@@ -385,7 +694,7 @@ class pluginBluditGithub extends Plugin
|
||||
return (is_array($result) && isset($result['sha'])) ? $result['sha'] : null;
|
||||
}
|
||||
|
||||
private function githubRequest($method, $endpoint, $data = null)
|
||||
protected function githubRequest($method, $endpoint, $data = null)
|
||||
{
|
||||
$token = $this->getValue('token');
|
||||
$owner = $this->getValue('owner');
|
||||
@@ -395,7 +704,10 @@ class pluginBluditGithub extends Plugin
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = 'https://api.github.com/repos/' . $owner . '/' . $repo . $endpoint;
|
||||
if ($method === 'GET' && strpos($endpoint, '/contents/') === 0 && strpos($endpoint, '?') === false) {
|
||||
$endpoint .= '?ref=' . rawurlencode($this->getValue('branch'));
|
||||
}
|
||||
$url = 'https://api.github.com/repos/' . rawurlencode($owner) . '/' . rawurlencode($repo) . $endpoint;
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
|
||||
Reference in new issue
Block a user