Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/plugin_vars.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"REPLACEME_CODENAME": {
"file": "/etc/os-release",
"key": "VERSION_CODENAME"
}
}
26 changes: 19 additions & 7 deletions installers/plugin_helper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
# @author billz
# license: GNU General Public License v3.0

# Exit on error
# Exit on error, including pipe failures
set -o errexit
set -o pipefail

readonly raspap_user="www-data"

Expand Down Expand Up @@ -166,19 +167,30 @@ case "$action" in

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

# add repository list if not present
if [ ! -f "$list_file" ]; then
echo "Adding repository $repo to sources list"
curl -fsSL "$repo" | sudo tee "$list_file" > /dev/null || { echo "Error: Failed to add repository to sources list."; exit 1; }
update_required=1
echo "Adding repository $repo to sources list"
if [[ "$repo" =~ ^https?:// ]]; then
# URL — download and write to sources list
curl -fsSL "$repo" | tee "$list_file" > /dev/null || { echo "Error: Failed to add repository to sources list."; exit 1; }
else
# Local file path — ensure it exists then copy
if [ ! -f "$repo" ]; then
echo "Error: Repository file not found: $repo"
exit 1
fi
cp "$repo" "$list_file" || { echo "Error: Failed to copy repository file to sources list."; exit 1; }
fi
update_required=1
else
echo "Repository already exists in sources list"
echo "Repository already exists in sources list"
fi

# update apt package list if required
Expand All @@ -199,7 +211,7 @@ case "$action" in
echo " config <source <destination> Applies a config file"
echo " javascript <source> <destination> Applies a JavaScript file"
echo " plugin <source> <destination> Copies a plugin directory"
echo " keys <key_url> <keyring> <repo> <sources> Installs a GPG key for a third-party repo"
echo " keys <key_url> <keyring> <repo> <sources> Installs a GPG key; repo is a URL or local file path"
exit 1
;;
esac
97 changes: 97 additions & 0 deletions src/RaspAP/Plugins/PluginInstaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class PluginInstaller
private $pluginsManifest;
private $repoPublic;
private $helperScriptPath;
private $pluginVarsPath;

public function __construct()
{
Expand All @@ -36,6 +37,7 @@ public function __construct()
$this->pluginsManifest = '/plugins/manifest.json';
$this->repoPublic = $this->getRepository();
$this->helperScriptPath = RASPI_CONFIG.'/plugins/plugin_helper.sh';
$this->pluginVarsPath = $this->rootPath . '/config/plugin_vars.json';
}

// Returns a single instance of PluginInstaller
Expand Down Expand Up @@ -152,6 +154,7 @@ public function installPlugin(string $pluginUri, string $pluginVersion, string $
$tempFile = null;
$extractDir = null;
$pluginDir = null;
$tempRepoFiles = [];

try {
if ($installPath === 'plugins-available') {
Expand Down Expand Up @@ -181,6 +184,8 @@ public function installPlugin(string $pluginUri, string $pluginVersion, string $
}

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

Expand All @@ -190,6 +195,33 @@ public function installPlugin(string $pluginUri, string $pluginVersion, string $
$rollbackStack[] = 'removeSudoers';
}
if (!empty($manifest['keys'])) {
foreach ($manifest['keys'] as &$keyEntry) {
$repo = $keyEntry['repo'];
if (strncmp($repo, 'http://', 7) !== 0 && strncmp($repo, 'https://', 8) !== 0) {
if (strncmp($repo, '/', 1) !== 0) {
$repo = $pluginDir . DIRECTORY_SEPARATOR . $repo;
}
$content = file_get_contents($repo);
if ($content === false) {
throw new \Exception("Failed to read repo file: $repo");
}
if (!empty($varMap)) {
$content = str_replace(array_keys($varMap), array_values($varMap), $content);
}
$tmpBase = tempnam(sys_get_temp_dir(), 'plugin_repo_');
if ($tmpBase === false) {
throw new \Exception("Failed to create temp file for repo");
}
unlink($tmpBase);
$tmpRepo = $tmpBase . '.list';
if (file_put_contents($tmpRepo, $content) === false) {
throw new \Exception("Failed to write temp repo file: $tmpRepo");
}
$tempRepoFiles[] = $tmpRepo;
$keyEntry['repo'] = $tmpRepo;
}
}
unset($keyEntry);
$this->installRepositoryKeys($manifest['keys']);
$rollbackStack[] = 'uninstallRepositoryKeys';
}
Expand Down Expand Up @@ -235,6 +267,11 @@ public function installPlugin(string $pluginUri, string $pluginVersion, string $
if (isset($extractDir) && is_dir($extractDir)) {
$this->deleteDir($extractDir);
}
foreach ($tempRepoFiles as $tmpRepo) {
if (file_exists($tmpRepo)) {
unlink($tmpRepo);
}
}
}
}

Expand Down Expand Up @@ -500,6 +537,66 @@ private function parseManifest($pluginDir): array
return $manifest;
}

/**
* Loads the token-to-system-value map from plugin_vars.json
*
* @return array flat map of token => resolved value
* @throws \Exception if a required key cannot be resolved
*/
private function loadVariableMap(): array
{
if (!file_exists($this->pluginVarsPath)) {
return [];
}

$json = file_get_contents($this->pluginVarsPath);
$entries = json_decode($json, true);

if (json_last_error() !== JSON_ERROR_NONE || !is_array($entries)) {
return [];
}

$map = [];
foreach ($entries as $token => $entry) {
$parsed = parse_ini_file($entry['file'], false, INI_SCANNER_RAW);
$key = $entry['key'];
if ($parsed === false || !isset($parsed[$key]) || $parsed[$key] === '') {
throw new \Exception(
"Cannot resolve $token: $key not found in {$entry['file']}"
);
}
$map[$token] = $parsed[$key];
}
return $map;
}

/**
* Substitutes tokens in all string values of a manifest array
*
* @param array $manifest
* @param array $varMap pre-loaded variable map
* @return array manifest with tokens replaced
* @throws \Exception
*/
private function resolveManifestVariables(array $manifest, array $varMap): array
{
if (empty($varMap)) {
return $manifest;
}

$walk = function ($value) use (&$walk, $varMap) {
if (is_string($value)) {
return str_replace(array_keys($varMap), array_values($varMap), $value);
}
if (is_array($value)) {
return array_map($walk, $value);
}
return $value;
};

return array_map($walk, $manifest);
}

/**
* Retrieves a plugin archive and extracts it to /tmp
*
Expand Down
Loading