Written with real angle brackets inside the inline block, it ended that element early, so the file rendered as visible text and no page JS ran at all.
177 lines
9.1 KiB
Plaintext
177 lines
9.1 KiB
Plaintext
Menu="Tasks:95"
|
|
Title="Varaverk"
|
|
Icon="varaverk.png"
|
|
---
|
|
<?php
|
|
$plugin = 'varaverk';
|
|
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
|
|
$pluginDir = "$docroot/plugins/$plugin";
|
|
|
|
require_once "$pluginDir/include/config.php";
|
|
?>
|
|
|
|
<script>
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
// CSRF token propagation — must run before any page JS.
|
|
//
|
|
// Unraid enforces CSRF centrally: /etc/php.ini sets auto_prepend_file to webGui's
|
|
// local_prepend.php, which terminates *every* POST that does not carry a valid token, before a
|
|
// single line of endpoint code runs. It accepts the token as a `csrf_token` POST field or as an
|
|
// X-CSRF-Token header.
|
|
//
|
|
// The webGUI's own injector is jQuery-only ($.ajaxPrefilter in HeadInlineJS.php). Varaverk's
|
|
// pages use native fetch(), which that prefilter does not touch — so without this shim every
|
|
// mutating request in the plugin is silently killed by the platform. Silently, because
|
|
// csrf_terminate() exits with no body: the fetch resolves, r.json() throws on the empty
|
|
// response, and the page's own .catch() swallows it.
|
|
//
|
|
// Setting the header rather than appending a body field is deliberate. It works identically for
|
|
// FormData, URLSearchParams and raw JSON bodies, so no call site has to know about it and a new
|
|
// endpoint cannot forget to include it. The header is only ever attached to same-origin
|
|
// Varaverk URLs; a cross-origin page cannot set a custom header without a preflight it will
|
|
// fail, which is precisely what makes this a CSRF defence rather than a formality.
|
|
//
|
|
// Inline, and above the tab content, because pages/*.php carry their own inline fetch calls and
|
|
// some fire on load — an external script could not be guaranteed to install first.
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
(function () {
|
|
if (window.__vvCsrfInstalled) return;
|
|
window.__vvCsrfInstalled = true;
|
|
|
|
var nativeFetch = window.fetch.bind(window);
|
|
|
|
window.fetch = function (input, init) {
|
|
var url = (typeof input === 'string') ? input : (input && input.url) || '';
|
|
|
|
if (url.indexOf('/plugins/varaverk/') !== -1 &&
|
|
typeof csrf_token !== 'undefined' && csrf_token) {
|
|
init = init || {};
|
|
var headers = new Headers(init.headers || (typeof input === 'object' && input.headers) || {});
|
|
if (!headers.has('X-CSRF-Token')) headers.set('X-CSRF-Token', csrf_token);
|
|
init = Object.assign({}, init, { headers: headers });
|
|
}
|
|
|
|
return nativeFetch(input, init);
|
|
};
|
|
})();
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
// Escaping helpers — shared, because every page builds HTML strings and assigns them to innerHTML.
|
|
//
|
|
// Defined here rather than per page for the reason the CSRF shim is: only one pages/*.php is ever
|
|
// included per request, so a helper defined inside one page does not exist for any other. That is
|
|
// not a hypothetical — these lived in pages/scheduler.php, and pages/monitor.php rendered media
|
|
// titles, partner hostnames and docker folder names straight into innerHTML with no escaping
|
|
// available to it at all.
|
|
//
|
|
// Not in js/varaverk.js, which would otherwise be the obvious home: that file is loaded by a
|
|
// src-tag *below* the tab include, so it is not defined yet while a page's inline script is
|
|
// running. The shared formatters there survive only because every caller is inside a fetch
|
|
// callback. An escaping helper must be callable from the first synchronous line of a page.
|
|
//
|
|
// Never write a literal script tag inside this block, not even in a comment. The tab body is
|
|
// injected into the page by the webGUI rather than parsed as part of the document, and the
|
|
// opening sequence ends this element early wherever it appears — the whole file then renders as
|
|
// visible text and every tab loads dead. That is not theoretical: the comment above said it with
|
|
// real angle brackets and took the monitor tab down on 2026-08-07.
|
|
//
|
|
// String() rather than assuming a string: these are fed payload fields that are frequently
|
|
// numbers, and sometimes null or undefined. A helper that throws on a number is a helper call
|
|
// sites will skip.
|
|
//
|
|
// Two functions because the contexts differ, and using the wrong one is silent:
|
|
// vvEscHtml text between tags. Leaves " alone — harmless there.
|
|
// vvEscAttr text inside an attribute. Escapes " as well, because a quote inside a
|
|
// double-quoted attribute ends it early and destroys the rest of the handler.
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
function vvEscHtml(s) {
|
|
return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
}
|
|
function vvEscAttr(s) {
|
|
return String(s ?? '').replace(/&/g,'&').replace(/"/g,'"')
|
|
.replace(/</g,'<').replace(/>/g,'>');
|
|
}
|
|
|
|
// A URL about to be put in href/src or handed to window.open. Anything that is not plainly http,
|
|
// https or a site-relative path becomes empty — javascript: is the one that matters, and an
|
|
// allowlist is the only way to say that without chasing encodings. include/docs.php applies the
|
|
// same rule to markdown links; container WebUI values, which come from template XML, had no such
|
|
// check before reaching window.open().
|
|
function vvSafeUrl(u) {
|
|
const s = String(u ?? '').trim();
|
|
if (s === '') return '';
|
|
if (/^https?:\/\//i.test(s)) return s;
|
|
if (/^\/(?!\/)/.test(s)) return s;
|
|
return '';
|
|
}
|
|
</script>
|
|
|
|
<?php
|
|
// First-run check — show setup wizard if HOST1 is blank OR local host.conf is missing
|
|
$_master = vv_read_conf_raw('master.conf');
|
|
preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $_master, $_h1m);
|
|
$_host1_blank = empty(trim($_h1m[1] ?? ''));
|
|
$_my_hostid = vv_detect_host();
|
|
$_conf_missing = $_my_hostid !== 'unknown'
|
|
&& !file_exists(CONF_DIR . '/' . $_my_hostid . '.conf');
|
|
if ($_host1_blank || $_conf_missing) {
|
|
include "$pluginDir/pages/setup.php";
|
|
return;
|
|
}
|
|
unset($_master, $_h1m, $_host1_blank, $_my_hostid, $_conf_missing);
|
|
|
|
// Determine active tab
|
|
$tab = $_GET['tab'] ?? 'monitor';
|
|
$validTabs = ['monitor', 'scheduler', 'docker', 'watchdog', 'partnership', 'fallback', 'arrs', 'rsync', 'auth', 'settings'];
|
|
|
|
// The AI tab exists only on HOST1, and only while AI_ENABLED is true. Appended to $validTabs
|
|
// rather than filtered out of it, so the check below rejects ?tab=ai server-side as well —
|
|
// omitting the link is presentation, not access control, and api/ai.php refuses every action
|
|
// on the same two conditions independently.
|
|
//
|
|
// The host half is not a preference: HOST1 owns the GPU, the Ollama process and the index, and
|
|
// include/ai.php only ever reads the *local* {HOST}_OLLAMA_URL — there is no Tailscale resolver
|
|
// in the PHP layer the way there is in the shell. On any other host the tab could only render
|
|
// and then fail its own health check.
|
|
$_vv_ai = vv_ai_ui_on();
|
|
if ($_vv_ai) $validTabs[] = 'ai';
|
|
|
|
if (!in_array($tab, $validTabs)) $tab = 'monitor';
|
|
$tabLabels = ['monitor' => 'Monitor', 'scheduler' => 'Scheduler', 'docker' => 'Docker', 'watchdog' => 'Watchdog', 'partnership' => 'Partnership', 'fallback' => 'FallBack', 'arrs' => 'Arrs', 'rsync' => 'Rsync', 'auth' => 'Auth Stack', 'settings' => 'Settings', 'ai' => 'AI'];
|
|
?>
|
|
|
|
<link rel="stylesheet" href="/plugins/<?=$plugin?>/css/varaverk.css">
|
|
|
|
<div id="varaverk-wrap" class="unapi">
|
|
|
|
<!-- Tab bar -->
|
|
<div id="vv-tabs">
|
|
<?php foreach ($validTabs as $t): ?>
|
|
<a href="?tab=<?=$t?>" class="localURL vv-tab<?= $t === $tab ? ' active' : '' ?>">
|
|
<?= $tabLabels[$t] ?? ucfirst($t) ?>
|
|
</a>
|
|
<?php endforeach; ?>
|
|
<div style="margin-left:auto;display:flex;align-items:center;gap:2px;">
|
|
<a href="https://github.com/FailedProxy/Varaverk" target="_blank"
|
|
style="padding:0 10px;font-size:10px;color:#333;text-decoration:none;
|
|
display:flex;align-items:center;letter-spacing:.03em;"
|
|
title="GitHub — source, issues, changelog">
|
|
⎋ GitHub
|
|
</a>
|
|
<button id="vv-expand-btn" type="button" onclick="vvToggleExpand()" title="Expand">⤢</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab content -->
|
|
<div id="vv-content">
|
|
<?php
|
|
$page = "$pluginDir/pages/$tab.php";
|
|
if (file_exists($page)) include $page;
|
|
else echo "<p>Page not found: $tab</p>";
|
|
?>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script src="/plugins/<?=$plugin?>/js/varaverk.js"></script>
|