Skip to content

Feat/add komga latest books homepage#2056

Open
JamesDAdams wants to merge 11 commits into
causefx:v2-masterfrom
JamesDAdams:feat/add-komga-latest-books-homepage
Open

Feat/add komga latest books homepage#2056
JamesDAdams wants to merge 11 commits into
causefx:v2-masterfrom
JamesDAdams:feat/add-komga-latest-books-homepage

Conversation

@JamesDAdams
Copy link
Copy Markdown
Contributor

A new plugin and homepage item for have komga latest books.

Copilot AI review requested due to automatic review settings March 27, 2026 12:27
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Komga integration intended to show recently added Komga books on the Organizr homepage by introducing a new plugin (API + settings + frontend JS) and registering a new homepage item/trait.

Changes:

  • Added a new komga plugin (settings, API endpoints, frontend rendering JS) and icon asset.
  • Added a Komga homepage item and registered it in Organizr via a new trait.
  • Updated Komga SSO “users/me” endpoint version and adjusted ignore rules / added an Apache rewrite file.

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
plugins/images/komga.svg Adds Komga icon used by the plugin/homepage item.
api/plugins/komga/plugin.php Defines plugin metadata and settings UI (including library/group override selection).
api/plugins/komga/plugin.json Adds plugin catalog metadata (repo, version, icon).
api/plugins/komga/main.js Fetches latest books and renders the homepage component in the browser.
api/plugins/komga/config.php Adds default config values for the Komga plugin.
api/plugins/komga/api.php Adds API endpoints for latest books, settings, and an image proxy.
api/homepage/komga.php Adds homepage item trait and container markup.
api/functions/sso-functions.php Updates Komga SSO endpoint path version.
api/classes/organizr.class.php Registers KomgaHomepageItem trait.
.htaccess Adds rewrite rules targeting /api/v2/index.php.
.gitignore Ensures komga plugin folder is committed; ignores local-dev.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread api/plugins/komga/api.php
Comment on lines +35 to +46
$res = curl_exec($ch);
curl_close($ch);

if ($res) {
$data = json_decode($res, true);
// Provide settings to JS via our API response
$GLOBALS['api']['response']['data'] = [
'title' => $komgaPlugin->config['KOMGA-title'] ?? 'Recently added books',
'baseUrl' => rtrim($url, '/') . '/book/',
'books' => (isset($data['content']) ? $data['content'] : $data),
'tabName' => $komgaPlugin->config['KOMGA-tab-name'] ?? 'Komga'
];
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cURL request for latest books has no connect/overall timeout, so a slow/unreachable Komga server can hang the API request and degrade homepage load. Set reasonable CURLOPT_CONNECTTIMEOUT/CURLOPT_TIMEOUT (similar to the 2s timeout used in settings) and handle curl_error/HTTP status codes to return a clean error response.

Suggested change
$res = curl_exec($ch);
curl_close($ch);
if ($res) {
$data = json_decode($res, true);
// Provide settings to JS via our API response
$GLOBALS['api']['response']['data'] = [
'title' => $komgaPlugin->config['KOMGA-title'] ?? 'Recently added books',
'baseUrl' => rtrim($url, '/') . '/book/',
'books' => (isset($data['content']) ? $data['content'] : $data),
'tabName' => $komgaPlugin->config['KOMGA-tab-name'] ?? 'Komga'
];
// Prevent hanging requests to Komga
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 2);
curl_setopt($ch, CURLOPT_TIMEOUT, 2);
$res = curl_exec($ch);
if ($res === false) {
$error = curl_error($ch);
curl_close($ch);
$GLOBALS['responseCode'] = 502;
$GLOBALS['api']['response']['error'] = 'Unable to fetch latest Komga books.';
$GLOBALS['api']['response']['errorDetails'] = $error;
} else {
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 200 && $httpCode < 300) {
$data = json_decode($res, true);
// Provide settings to JS via our API response
$GLOBALS['api']['response']['data'] = [
'title' => $komgaPlugin->config['KOMGA-title'] ?? 'Recently added books',
'baseUrl' => rtrim($url, '/') . '/book/',
'books' => (isset($data['content']) ? $data['content'] : $data),
'tabName' => $komgaPlugin->config['KOMGA-tab-name'] ?? 'Komga'
];
} else {
$GLOBALS['responseCode'] = 502;
$GLOBALS['api']['response']['error'] = 'Komga responded with an unexpected status code.';
$GLOBALS['api']['response']['statusCode'] = $httpCode;
}

Copilot uses AI. Check for mistakes.
Comment thread api/plugins/komga/api.php
if ($url && $apiKey) {
$endpoint = '/api/v1/books?sort=createdDate,desc&size=20';
if ($libraries && $libraries !== 'all') {
$endpoint .= '&library_id=' . $libraries;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$libraries is concatenated into the query string without URL encoding. If library IDs ever contain characters like &/? this will break the request and could allow parameter injection. Use rawurlencode($libraries) (or build the URL with a query array) when appending library_id.

Suggested change
$endpoint .= '&library_id=' . $libraries;
$endpoint .= '&library_id=' . rawurlencode($libraries);

Copilot uses AI. Check for mistakes.
Comment thread api/plugins/komga/main.js
Comment on lines +60 to +66
let html = `
<div class="row" style="margin-bottom: 30px; margin-top: 20px;">
<div class="col-lg-12">
<div style="display: flex; align-items: center; margin-bottom: 15px; padding-left: 5px;">
<img src="plugins/images/komga.svg" style="width: 22px; height: 22px; margin-right: 10px;" onerror="this.style.display='none'">
<span style="text-transform: uppercase; font-weight: bold; font-size: 13px; letter-spacing: 1px; color: #eee;" lang="en">${titleDisplay}</span>
</div>
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code builds HTML with template literals containing titleDisplay, title, series, etc. from Komga and then assigns it via innerHTML. If any of those fields contain HTML (or quotes), this becomes an XSS vector in Organizr. Prefer constructing DOM nodes and assigning user-controlled text via textContent (or sanitize/escape all interpolated values) before inserting into the document.

Copilot uses AI. Check for mistakes.
Comment thread api/plugins/komga/main.js
Comment on lines +42 to +47
sideBarLinks.forEach(link => {
if (link.textContent.trim() === KOMGA_TAB_NAME || link.textContent.includes('Komga')) {
link.click();
found = true;
}
});
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sideBarLinks.forEach(...) can click multiple matching links because there’s no way to break out early once found is true. This can cause unexpected navigation if multiple items contain “Komga”. Use a for...of loop (or Array.prototype.find) so you can stop after the first match, and match the tab more precisely (e.g., exact match on the configured tab name).

Suggested change
sideBarLinks.forEach(link => {
if (link.textContent.trim() === KOMGA_TAB_NAME || link.textContent.includes('Komga')) {
link.click();
found = true;
}
});
for (const link of sideBarLinks) {
const text = (link.textContent || '').trim();
if (text && text.toLowerCase() === KOMGA_TAB_NAME.toLowerCase()) {
link.click();
found = true;
break;
}
}

Copilot uses AI. Check for mistakes.
Comment thread api/plugins/komga/main.js
const blob = await res.blob();
const objectURL = URL.createObjectURL(blob);
const el = document.getElementById(elementId);
if (el) el.style.backgroundImage = `url('${objectURL}')`;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL.createObjectURL(blob) is never revoked, so navigating around or reloading the component can leak memory over time. Consider revoking the object URL after the image is applied (e.g., after a short delay or once the image has been used) to avoid accumulation.

Suggested change
if (el) el.style.backgroundImage = `url('${objectURL}')`;
if (el) {
el.style.backgroundImage = `url('${objectURL}')`;
// Revoke the object URL after a short delay to avoid memory leaks.
setTimeout(() => {
URL.revokeObjectURL(objectURL);
}, 1000);
}

Copilot uses AI. Check for mistakes.
Comment thread .htaccess
RewriteBase /api/v2
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /api/v2/index.php [QSA,L]
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rewrite rule will route all requests (including the UI, static assets, etc.) to /api/v2/index.php because it matches ^ at the repo root. That will likely break normal page loads and asset serving. Restrict this rewrite to only /api/v2/* paths, or move this file into api/v2/.htaccess and rewrite to that directory’s index.php with an appropriate RewriteBase.

Suggested change
RewriteRule ^ /api/v2/index.php [QSA,L]
RewriteRule ^api/v2/ /api/v2/index.php [QSA,L]

Copilot uses AI. Check for mistakes.
Comment thread api/plugins/komga/api.php
Comment on lines +62 to +74
if ($komgaPlugin->qualifyRequest(999, true)) {
$apiUrl = $komgaPlugin->config['KOMGA-url'] ?? '';
$apiKey = $komgaPlugin->config['KOMGA-apikey'] ?? '';

// Allow full URL (if passed via query string) or just append to Komga API URL
$urlParams = $request->getQueryParams();
$thumbnailUrl = $urlParams['url'] ?? '';

if ($apiUrl && $apiKey && $thumbnailUrl) {
// Determine if thumbnailUrl is absolute or relative
if (!preg_match('~^(?:f|ht)tps?://~i', $thumbnailUrl)) {
$thumbnailUrl = rtrim($apiUrl, '/') . '/' . ltrim($thumbnailUrl, '/');
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/plugins/komga/image accepts an arbitrary url query param and, when absolute URLs are allowed, will forward the Komga X-API-Key header to that host. This is an SSRF vector and can leak the Komga API key to attacker-controlled domains; additionally, the route is currently accessible to guests (qualifyRequest(999)). Restrict access to at least the plugin’s configured auth level, and validate that the requested URL is only within the configured Komga base URL (e.g., same scheme/host/port and expected path prefix) before proxying.

Copilot uses AI. Check for mistakes.
Comment thread api/plugins/komga/api.php
Comment on lines +82 to +86
$res = curl_exec($ch);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);

if ($res) {
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image proxy cURL request also lacks connect/overall timeouts and doesn’t check for cURL errors or non-2xx HTTP status codes. Add timeouts and validate the upstream response code to avoid tying up PHP workers and to prevent caching/serving error pages as images.

Suggested change
$res = curl_exec($ch);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
if ($res) {
// Set reasonable timeouts to avoid tying up PHP workers
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$res = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$curlError = curl_errno($ch);
curl_close($ch);
if ($res !== false && $curlError === 0 && $httpCode >= 200 && $httpCode < 300) {

Copilot uses AI. Check for mistakes.
Comment thread api/plugins/komga/main.js
Comment on lines +76 to +79
html += `
<div style="flex: 0 0 150px; max-width: 150px; cursor: pointer;" onclick="komgaOpenBook('${komgaBaseUrl}', '${book.id}')">
<div id="${imgId}" class="komga-card-hover"
style="background-color: #333; background-size: cover; background-position: center; width: 100%; aspect-ratio: 2/3; border-radius: 4px; box-shadow: 0 4px 10px rgba(0,0,0,0.5); margin-bottom: 8px; transition: transform 0.2s ease;">
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onclick="komgaOpenBook('${komgaBaseUrl}', '${book.id}')" injects dynamic values into an inline handler without escaping. If either value contains quotes it can break the attribute and enable script injection. Attach click listeners with addEventListener (and keep data in closures / data-* attributes) rather than building inline JS in HTML strings.

Copilot uses AI. Check for mistakes.
'KOMGA-url' => '',
'KOMGA-apikey' => '',
'KOMGA-libraries' => 'all',
'KOMGA-title' => 'Recently added books'
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KOMGA-tab-name is used in the plugin settings/API response but isn’t present in the default config array. Adding it here (with the same default as the settings UI) avoids undefined config keys and makes defaults consistent across fresh installs.

Suggested change
'KOMGA-title' => 'Recently added books'
'KOMGA-title' => 'Recently added books',
'KOMGA-tab-name' => ''

Copilot uses AI. Check for mistakes.
@causefx
Copy link
Copy Markdown
Owner

causefx commented May 19, 2026

is this ready to merge?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants