Skip to content

Commit 8d40fba

Browse files
committed
feat: add dynamic OS codename resolution for plugin repo sources
Introduce config/plugin_vars.json to map manifest tokens to system values read from /etc/os-release. PluginInstaller now resolves tokens in local .list repo files before passing them to plugin_helper.sh, enabling plugins like Docker to target the correct apt repository for any Debian-based OS without hardcoding a codename. Extend plugin_helper.sh keys action to accept a local file path (in addition to a URL) as the repo argument. Also add pipefail and automatic keyring directory creation for robustness.
1 parent dd9a148 commit 8d40fba

File tree

3 files changed

+122
-7
lines changed

3 files changed

+122
-7
lines changed

config/plugin_vars.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"REPLACEME_CODENAME": {
3+
"file": "/etc/os-release",
4+
"key": "VERSION_CODENAME"
5+
}
6+
}

installers/plugin_helper.sh

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
# @author billz
55
# license: GNU General Public License v3.0
66

7-
# Exit on error
7+
# Exit on error, including pipe failures
88
set -o errexit
9+
set -o pipefail
910

1011
readonly raspap_user="www-data"
1112

@@ -166,19 +167,30 @@ case "$action" in
166167

167168
# add repository GPG key if it doesn't already exist
168169
if [ ! -f "$keyring" ]; then
170+
mkdir -p "$(dirname "$keyring")"
169171
echo "Downloading GPG key from $key_url..."
170-
curl -fsSL "$key_url" | sudo tee "$keyring" > /dev/null || { echo "Error: Failed to download GPG key."; exit 1; }
172+
curl -fsSL "$key_url" | tee "$keyring" > /dev/null || { echo "Error: Failed to download GPG key."; exit 1; }
171173
else
172174
echo "Repository GPG key already exists at $keyring"
173175
fi
174176

175177
# add repository list if not present
176178
if [ ! -f "$list_file" ]; then
177-
echo "Adding repository $repo to sources list"
178-
curl -fsSL "$repo" | sudo tee "$list_file" > /dev/null || { echo "Error: Failed to add repository to sources list."; exit 1; }
179-
update_required=1
179+
echo "Adding repository $repo to sources list"
180+
if [[ "$repo" =~ ^https?:// ]]; then
181+
# URL — download and write to sources list
182+
curl -fsSL "$repo" | tee "$list_file" > /dev/null || { echo "Error: Failed to add repository to sources list."; exit 1; }
183+
else
184+
# Local file path — ensure it exists then copy
185+
if [ ! -f "$repo" ]; then
186+
echo "Error: Repository file not found: $repo"
187+
exit 1
188+
fi
189+
cp "$repo" "$list_file" || { echo "Error: Failed to copy repository file to sources list."; exit 1; }
190+
fi
191+
update_required=1
180192
else
181-
echo "Repository already exists in sources list"
193+
echo "Repository already exists in sources list"
182194
fi
183195

184196
# update apt package list if required
@@ -199,7 +211,7 @@ case "$action" in
199211
echo " config <source <destination> Applies a config file"
200212
echo " javascript <source> <destination> Applies a JavaScript file"
201213
echo " plugin <source> <destination> Copies a plugin directory"
202-
echo " keys <key_url> <keyring> <repo> <sources> Installs a GPG key for a third-party repo"
214+
echo " keys <key_url> <keyring> <repo> <sources> Installs a GPG key; repo is a URL or local file path"
203215
exit 1
204216
;;
205217
esac

src/RaspAP/Plugins/PluginInstaller.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class PluginInstaller
2424
private $pluginsManifest;
2525
private $repoPublic;
2626
private $helperScriptPath;
27+
private $pluginVarsPath;
2728

2829
public function __construct()
2930
{
@@ -36,6 +37,7 @@ public function __construct()
3637
$this->pluginsManifest = '/plugins/manifest.json';
3738
$this->repoPublic = $this->getRepository();
3839
$this->helperScriptPath = RASPI_CONFIG.'/plugins/plugin_helper.sh';
40+
$this->pluginVarsPath = $this->rootPath . '/config/plugin_vars.json';
3941
}
4042

4143
// Returns a single instance of PluginInstaller
@@ -152,6 +154,7 @@ public function installPlugin(string $pluginUri, string $pluginVersion, string $
152154
$tempFile = null;
153155
$extractDir = null;
154156
$pluginDir = null;
157+
$tempRepoFiles = [];
155158

156159
try {
157160
if ($installPath === 'plugins-available') {
@@ -181,6 +184,8 @@ public function installPlugin(string $pluginUri, string $pluginVersion, string $
181184
}
182185

183186
$manifest = $this->parseManifest($pluginDir);
187+
$varMap = $this->loadVariableMap();
188+
$manifest = $this->resolveManifestVariables($manifest, $varMap);
184189
$this->pluginName = preg_replace('/\s+/', '', $manifest['name']);
185190
$rollbackStack = []; // Store actions to rollback on failure
186191

@@ -190,6 +195,33 @@ public function installPlugin(string $pluginUri, string $pluginVersion, string $
190195
$rollbackStack[] = 'removeSudoers';
191196
}
192197
if (!empty($manifest['keys'])) {
198+
foreach ($manifest['keys'] as &$keyEntry) {
199+
$repo = $keyEntry['repo'];
200+
if (strncmp($repo, 'http://', 7) !== 0 && strncmp($repo, 'https://', 8) !== 0) {
201+
if (strncmp($repo, '/', 1) !== 0) {
202+
$repo = $pluginDir . DIRECTORY_SEPARATOR . $repo;
203+
}
204+
$content = file_get_contents($repo);
205+
if ($content === false) {
206+
throw new \Exception("Failed to read repo file: $repo");
207+
}
208+
if (!empty($varMap)) {
209+
$content = str_replace(array_keys($varMap), array_values($varMap), $content);
210+
}
211+
$tmpBase = tempnam(sys_get_temp_dir(), 'plugin_repo_');
212+
if ($tmpBase === false) {
213+
throw new \Exception("Failed to create temp file for repo");
214+
}
215+
unlink($tmpBase);
216+
$tmpRepo = $tmpBase . '.list';
217+
if (file_put_contents($tmpRepo, $content) === false) {
218+
throw new \Exception("Failed to write temp repo file: $tmpRepo");
219+
}
220+
$tempRepoFiles[] = $tmpRepo;
221+
$keyEntry['repo'] = $tmpRepo;
222+
}
223+
}
224+
unset($keyEntry);
193225
$this->installRepositoryKeys($manifest['keys']);
194226
$rollbackStack[] = 'uninstallRepositoryKeys';
195227
}
@@ -235,6 +267,11 @@ public function installPlugin(string $pluginUri, string $pluginVersion, string $
235267
if (isset($extractDir) && is_dir($extractDir)) {
236268
$this->deleteDir($extractDir);
237269
}
270+
foreach ($tempRepoFiles as $tmpRepo) {
271+
if (file_exists($tmpRepo)) {
272+
unlink($tmpRepo);
273+
}
274+
}
238275
}
239276
}
240277

@@ -500,6 +537,66 @@ private function parseManifest($pluginDir): array
500537
return $manifest;
501538
}
502539

540+
/**
541+
* Loads the token-to-system-value map from plugin_vars.json
542+
*
543+
* @return array flat map of token => resolved value
544+
* @throws \Exception if a required key cannot be resolved
545+
*/
546+
private function loadVariableMap(): array
547+
{
548+
if (!file_exists($this->pluginVarsPath)) {
549+
return [];
550+
}
551+
552+
$json = file_get_contents($this->pluginVarsPath);
553+
$entries = json_decode($json, true);
554+
555+
if (json_last_error() !== JSON_ERROR_NONE || !is_array($entries)) {
556+
return [];
557+
}
558+
559+
$map = [];
560+
foreach ($entries as $token => $entry) {
561+
$parsed = parse_ini_file($entry['file'], false, INI_SCANNER_RAW);
562+
$key = $entry['key'];
563+
if ($parsed === false || !isset($parsed[$key]) || $parsed[$key] === '') {
564+
throw new \Exception(
565+
"Cannot resolve $token: $key not found in {$entry['file']}"
566+
);
567+
}
568+
$map[$token] = $parsed[$key];
569+
}
570+
return $map;
571+
}
572+
573+
/**
574+
* Substitutes tokens in all string values of a manifest array
575+
*
576+
* @param array $manifest
577+
* @param array $varMap pre-loaded variable map
578+
* @return array manifest with tokens replaced
579+
* @throws \Exception
580+
*/
581+
private function resolveManifestVariables(array $manifest, array $varMap): array
582+
{
583+
if (empty($varMap)) {
584+
return $manifest;
585+
}
586+
587+
$walk = function ($value) use (&$walk, $varMap) {
588+
if (is_string($value)) {
589+
return str_replace(array_keys($varMap), array_values($varMap), $value);
590+
}
591+
if (is_array($value)) {
592+
return array_map($walk, $value);
593+
}
594+
return $value;
595+
};
596+
597+
return array_map($walk, $manifest);
598+
}
599+
503600
/**
504601
* Retrieves a plugin archive and extracts it to /tmp
505602
*

0 commit comments

Comments
 (0)