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:
interlope committed 2026-11-01 18:33:07 +01:00
1 parent feefb08a85
commit 3dfd367814
6 files changed
+848 -80

No files matched your search

+339 -27
View File
@@ -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, [