mirror of
https://github.com/unraid/webgui.git
synced 2025-12-31 06:30:10 -06:00
916 lines
32 KiB
PHP
Executable File
916 lines
32 KiB
PHP
Executable File
#!/usr/bin/php -q
|
|
<?PHP
|
|
// Copyright 2005-2025, Lime Technology
|
|
// License: GPLv2 only
|
|
//
|
|
// Program updates made by Bergware International (April 2020)
|
|
// Program updates made by Bergware International (June 2022)
|
|
|
|
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
|
|
require_once "$docroot/webGui/include/Wrappers.php";
|
|
require_once "$docroot/webGui/include/publish.php";
|
|
$logger = 'plugin-manager';
|
|
|
|
$usage = <<<EOF
|
|
Process plugin files.
|
|
|
|
Usage: plugin install PLUGIN-FILE [forced]
|
|
install a plugin
|
|
|
|
PLUGIN-FILE is a plugin definition XML file with ".plg" extension.
|
|
|
|
PLUGIN-FILE can be a local file, or a URL. If a URL, the plugin file is first downloaded to /tmp/plugins.
|
|
|
|
forced is optional and can be used to install a lower version than currently running.
|
|
|
|
Usage: plugin download PLUGIN-FILE [forced]
|
|
download plugin files without executing any Run commands defined for the install method
|
|
|
|
PLUGIN-FILE is a plugin definition XML file with ".plg" extension.
|
|
|
|
PLUGIN-FILE can be a local file, or a URL. If a URL, the plugin file is first downloaded to /tmp/plugins.
|
|
|
|
forced is optional and can be used to download a lower version than currently running.
|
|
|
|
Both install and download commands will process all FILE elements in PLUGIN-FILE which are tagged with the
|
|
\"install\" method (or that have no method tag).
|
|
|
|
This command has two major use cases:
|
|
|
|
1) Invoked at system startup by /etc/rc.d/rc.local on each .plg file found int /boot/config/plugins.
|
|
|
|
Upon success we register the plugin as "installed" by creating a symlink to it in /var/log/plugins.
|
|
|
|
If any kind of error, we move the file to /boot/config/plugins-error.
|
|
|
|
If a symlink already exists for the plugin file, this indicates a plugin replacing a "built-in" plugin. In
|
|
this case, if the version of PLUGIN-FILE is newer than the built-in plugin, we go ahead and install normally;
|
|
otherwise, we move to /boot/config/plugins-stale.
|
|
|
|
2) Invoked manually or via Plugin Manager for a .plg file not in /boot/config/plugins.
|
|
|
|
If a symlink already exists for the plugin file, this indicates a plugin update. In this case, if the version of
|
|
PLUGIN-FILE is newer than the built-in plugin, we go ahead and install normally and then move the old plugin
|
|
to /boot/config/plugins-stale.
|
|
|
|
Upon success we copy PLUGIN-FILE to /boot/config/plugins and register it as "installed" by creating a
|
|
symlink to it in /var/log/plugins.
|
|
|
|
Usage: plugin remove PLUGIN
|
|
remove a plugin
|
|
|
|
PLUGIN is the file basename of a plugin, e.g., "myplugin.plg".
|
|
|
|
If PLUGIN is found in /var/log/plugins then this command will process all FILE elements in PLUGIN which are
|
|
tagged with the "remove" method. Upon success we delete /var/log/plugins/PLUGIN and move the plugin
|
|
file to /boot/config/plugins-removed
|
|
|
|
Usage: plugin check PLUGIN
|
|
check and output the latest version of PLUGIN
|
|
|
|
We extract the pluginURL attribute from PLUGIN and use it to download (presumably the latest
|
|
version of) the plugin file to /tmp/plugins/ directory, and then output the version string.
|
|
|
|
Usage: plugin checkall
|
|
check all installed plugins
|
|
|
|
Runs 'plugin check PLUGIN' for each plugin file linked-to in /var/log/plugins.
|
|
|
|
Usage: plugin update PLUGIN
|
|
update the plugin
|
|
|
|
We look for the new plugin in /tmp/plugins/ directory. If found then we first execute the "install"
|
|
method of each FILE in the new plugin. (If necessary, a plugin can detect that this is an
|
|
"update" by checking for the existence of /var/log/plugins/PLUGIN.) If successful, we
|
|
delete the "old" plugin file from /boot/config/plugins/, copy the "new" plugin file from
|
|
/tmp/plugins/ to /boot/config/plugins/, and finally create the new symlink.
|
|
|
|
Note: to support `plugin check` and `plugin update` the plugin file must contain both "pluginURL" and
|
|
"version" attributes.
|
|
|
|
Usage: plugin [attribute name] PLUGIN-FILE
|
|
|
|
Any method which is not one of the actions listed above is assumed to be the name of an attribute of
|
|
the <PLUGIN> tag within PLUGIN-FILE. If the attribute exists, its value (a string) is output and the command
|
|
exit status is 0. If the attribute does not exist, command exit status is 1.
|
|
|
|
The plugin manager recognizes this set of attributes for the <PLUGIN> tag:
|
|
|
|
name - MANDATORY plugin name, e.g., "myplugin" or "my-plugin" etc.
|
|
This tag defines the name of the plugin. The name should omit embedded information such as architecture,
|
|
version, author, etc.
|
|
|
|
The plugin should create a directory under `/usr/local/emhttp/plugins` named after
|
|
the plugin, e.g., `/usr/local/emhttp/plugins/myplugin`. Any webGui pages, icons, README files, etc, should
|
|
be created inside this directory.
|
|
|
|
The plugin should also create a directory under `/boot/config/plugins` named after the plugin, e.g.,
|
|
`/boot/config/plugins/myplugin`. Here is where you store plugin-specific files such as a configuration
|
|
file and icon file. Note that this directory exists on the users USB Flash device and writes should be
|
|
minimized.
|
|
|
|
Upon successful installation, the plugin manager will copy the input plugin file to `/boot/config/plugins`
|
|
on the users USB Flash device, and create a symlink in `/var/log/plugins` also named after the plugin,
|
|
e.g., `/var/log/plugins/myplugin`. Each time the unRaid server is re-booted, all plugins stored
|
|
in `/boot/config/plugins` are automatically installed; plugin authors should be aware of this behavior.
|
|
|
|
author - OPTIONAL
|
|
Whatever you put here will show up under the **Author** column in the Plugin List. If this attribute
|
|
is omitted we display "anonymous".
|
|
|
|
version - MANDATORY
|
|
Use a string suitable for comparison to determine if one version is older/same/newer than another version.
|
|
Any format is acceptable but LimeTech uses "YYYY.MM.DD", e.g., "2014.02.18" (if multiple versions happen
|
|
to get posted on the same day we add a letter suffix, e.g., "2014.02.18a").
|
|
|
|
pluginURL - OPTIONAL but MANDATORY if you want "check for updates" to work with your plugin
|
|
This is the URL of the plugin file to download and extract the **version** attribute from to determine if
|
|
this is a new version.
|
|
|
|
More attributes may be defined in the future.
|
|
|
|
Here is the set of directories and files used by the plugin system:
|
|
|
|
/boot/config/plugins/
|
|
This directory contains the plugin files for plugins to be (re)installed at boot-time. Upon
|
|
successful `plugin install`, the plugin file is copied here (if not here already). Upon successful
|
|
`plugin remove`, the plugin file is deleted from here.
|
|
|
|
/boot/config/plugins-error/
|
|
This directory contains plugin files that failed to install.
|
|
|
|
/boot/config/plugins-removed/
|
|
This directory contains plugin files that have been removed.
|
|
|
|
/boot/config/plugins-stale/
|
|
This directory contains plugin files that failed to install because a newer version of the same plugin is
|
|
already installed.
|
|
|
|
/tmp/plugins/
|
|
This directory is used as a target for downloaded plugin files. The `plugin check` operation
|
|
downloads the plugin file here and the `plugin update` operation looks for the plugin to update here.
|
|
|
|
/var/log/plugins/
|
|
This directory contains a symlink named after the plugin name (not the plugin file name) which points to
|
|
the actual plugin file used to install the plugin. The existence of this file indicates successful
|
|
install of the plugin.
|
|
|
|
EOF;
|
|
|
|
// Error code to description (wget)
|
|
// ref: https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html
|
|
//
|
|
function error_desc($code) {
|
|
switch($code) {
|
|
case 0: return 'No errors';
|
|
case -1: return 'Generic error';
|
|
case 1: return 'Generic error';
|
|
case 2: return 'Parse error';
|
|
case 3: return 'File I/O error';
|
|
case 4: return 'Network failure';
|
|
case 5: return 'SSL verification failure - Check the date and time of your server in Settings - Date And Time';
|
|
case 6: return 'Username/password authentication failure';
|
|
case 7: return 'Protocol errors';
|
|
case 8: return 'Invalid URL / Server error response';
|
|
default: return 'Error code '.$code;
|
|
}
|
|
}
|
|
|
|
// Signal DONE to caller
|
|
//
|
|
function done($code) {
|
|
global $nchan;
|
|
if ($nchan) write('_DONE_','');
|
|
exit($code);
|
|
}
|
|
|
|
// Function to write either to console (echo) or nchan (curl)
|
|
// Default output is console, use optional parameter "nchan" to write to nchan instead
|
|
//
|
|
function write(...$messages){
|
|
global $nchan;
|
|
if ($nchan) {
|
|
foreach ($messages as $message) {
|
|
publish('plugins', $message,1,false);
|
|
}
|
|
} else {
|
|
foreach ($messages as $message) echo $message;
|
|
}
|
|
}
|
|
|
|
// Run command and obtain output
|
|
//
|
|
function run($command) {
|
|
$run = popen($command,'r');
|
|
while (!feof($run)) write(fgets($run));
|
|
return pclose($run);
|
|
}
|
|
|
|
// Run hooked scripts before correct execution of "method"
|
|
// method = install, update, remove, check
|
|
// hook programs receives three parameters: type=plugin and method and plugin-name
|
|
//
|
|
function pre_hooks() {
|
|
global $method, $plugin, $download_only;
|
|
if ($download_only) return;
|
|
$hooks = "/usr/local/emhttp/plugins/dynamix.plugin.manager/pre-hooks";
|
|
foreach (glob("$hooks/*") as $hook) if (is_executable($hook)) {
|
|
write("Executing hook script: ".basename($hook)."\n");
|
|
run("$hook plugin $method $plugin");
|
|
}
|
|
}
|
|
|
|
// Run hooked scripts after successful or failed completion of "method"
|
|
// method = install, update, remove, check
|
|
// hook programs receives four parameters: type=plugin and method and plugin-name and error (empty if none)
|
|
//
|
|
function post_hooks($error='') {
|
|
global $method, $plugin, $download_only;
|
|
if ($download_only) return;
|
|
$hooks = "/usr/local/emhttp/plugins/dynamix.plugin.manager/post-hooks";
|
|
foreach (glob("$hooks/*") as $hook) if (is_executable($hook)) {
|
|
write("Executing hook script: ".basename($hook)."\n");
|
|
run("$hook plugin $method $plugin $error");
|
|
}
|
|
}
|
|
|
|
// Download a file from a URL.
|
|
// Returns TRUE if success else FALSE and fills in error.
|
|
//
|
|
function download($url, $name, &$error, $write=true) {
|
|
if ($url) {
|
|
$plg = basename($url);
|
|
$plg = str_replace('"', '', $plg);
|
|
$tries = (strpos($url, '.cdn') !== false) ? "1" : "3";
|
|
if ($file = popen("wget --compression=auto --no-cache --progress=dot --retry-connrefused --prefer-family=IPv4 --timeout=10 --tries=$tries --waitretry=$tries -O $name $url 2>&1", 'r')) {
|
|
if ($write) write("plugin: downloading: $plg ...\r");
|
|
$level = -1;
|
|
while (($line = fgets($file)) !== false) {
|
|
if (preg_match('/ \d+% /', $line, $matches)) {
|
|
$percentage = substr(trim($matches[0]),0,-1);
|
|
if ($percentage > $level) {
|
|
if ($write) write("plugin: downloading: $plg ... $percentage%\r");
|
|
$level = $percentage;
|
|
}
|
|
}
|
|
}
|
|
if (($perror = pclose($file)) != 0) {
|
|
$error = "$plg download failure: ".error_desc($perror);
|
|
return false;
|
|
} elseif (filesize($name) == 0) {
|
|
if ($write) write("plugin: download failure: zero-length file\r","\n");
|
|
return false;
|
|
} else {
|
|
if ($write) write("plugin: downloading: $plg ... done\r","\n");
|
|
return true;
|
|
}
|
|
} else {
|
|
$error = "$plg failed to open";
|
|
return false;
|
|
}
|
|
} else {
|
|
// The url is empty
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Filter URL.
|
|
// If URL contains '.cdn' remove the '.cdn' from the URL.
|
|
// Returns URL without '.cdn' if found.
|
|
//
|
|
function filter_url($url) {
|
|
if (strpos($url, '.cdn') !== false) {
|
|
$new_url = str_replace('"', '', $url);
|
|
$new_url = str_replace('.cdn', '', $new_url);
|
|
} else {
|
|
$new_url = "";
|
|
}
|
|
return($new_url);
|
|
}
|
|
|
|
// Interpret a plugin file
|
|
// Returns TRUE if success, else FALSE and fills in error string.
|
|
//
|
|
// If a FILE element does not have a Method attribute, we treat as though Method is "install".
|
|
// A FILE Method attribute can list multiple methods separated by spaces in which case that file
|
|
// is processed for any of those methods.
|
|
//
|
|
function plugin($method, $plugin_file, &$error) {
|
|
global $unraid, $logger, $download_only;
|
|
$methods = ['install', 'remove'];
|
|
|
|
// parse plugin definition XML file
|
|
$xml = file_exists($plugin_file) ? @simplexml_load_file($plugin_file, NULL, LIBXML_NOCDATA) : false;
|
|
if ($xml === false) {
|
|
$error = "XML file doesn't exist or xml parse error";
|
|
return false;
|
|
}
|
|
|
|
// dump
|
|
if ($method == 'dump') {
|
|
// dump file: debugging
|
|
write(print_r($xml,true));
|
|
return true;
|
|
}
|
|
|
|
// release notes
|
|
if ($method == 'changes') {
|
|
return $xml->CHANGES ? trim($xml->CHANGES) : false;
|
|
}
|
|
|
|
// alert message
|
|
if ($method == 'alert') {
|
|
return $xml->ALERT ? trim($xml->ALERT) : false;
|
|
}
|
|
|
|
// validate plugin download without installation
|
|
if ($method == 'validate') {
|
|
$name = '/tmp/validate-plugin.tmp';
|
|
foreach ($xml->FILE as $file) if ($file->URL) {
|
|
if (!$file->SHA256 and !$file->MD5) continue;
|
|
if ( (download($file->URL, $name, $error, false) === false) && (download(filter_url($file->URL), $name, $error, false) === false) ) {
|
|
@unlink($name);
|
|
return false;
|
|
}
|
|
if (($file->SHA256 && hash_file('sha256',$name) != $file->SHA256) or ($file->MD5 && md5_file($name) != $file->MD5)) {
|
|
$error = "bad hash value";
|
|
@unlink($name);
|
|
return false;
|
|
}
|
|
}
|
|
@unlink($name);
|
|
return "valid";
|
|
}
|
|
|
|
// check if $method is an attribute
|
|
if (!in_array($method, $methods)) {
|
|
foreach ($xml->attributes() as $key => $value) {
|
|
if ($method == $key) return $value;
|
|
}
|
|
$error = "$method attribute not present";
|
|
return false;
|
|
}
|
|
|
|
// Process FILE elements in order
|
|
//
|
|
$current_file = 0;
|
|
foreach ($xml->FILE as $file) {
|
|
$current_file++;
|
|
// skip if not our $method
|
|
if (isset($file->attributes()->Method)) {
|
|
if (!in_array($method, explode(" ", $file->attributes()->Method))) continue;
|
|
} elseif ($method != 'install') continue;
|
|
$name = $file->attributes()->Name ?: '';
|
|
// bergware - check Unraid version dependency (if present)
|
|
$min = $file->attributes()->Min;
|
|
if ($min && version_compare($unraid['version'],$min,'<')) {
|
|
write("plugin: skipping: ".basename($name)." - Unraid version too low, requires at least version $min\n");
|
|
continue;
|
|
}
|
|
$max = $file->attributes()->Max;
|
|
if ($max && version_compare($unraid['version'],$max,'>')) {
|
|
write("plugin: skipping: ".basename($name)." - Unraid version too high, requires at most version $max\n");
|
|
continue;
|
|
}
|
|
// Name can be missing but only makes sense if Run attribute is present
|
|
if ($name) {
|
|
// Ensure parent directory exists
|
|
//
|
|
if (!file_exists(dirname($name))) {
|
|
if (!mkdir(dirname($name), 0770, true)) {
|
|
$error = "unable to create parent directory for $name";
|
|
return false;
|
|
}
|
|
}
|
|
// If file already exists, check the SHA256/MD5 (if supplied)
|
|
if (file_exists($name)) {
|
|
if ($file->SHA256) {
|
|
my_logger("checking: $name - SHA256", $logger);
|
|
if (hash_file('sha256', $name) != $file->SHA256) {
|
|
unlink($name);
|
|
}
|
|
} elseif ($file->MD5) {
|
|
my_logger("checking: $name - MD5", $logger);
|
|
if (md5_file($name) != $file->MD5) {
|
|
unlink($name);
|
|
}
|
|
}
|
|
}
|
|
// If file already exists, do not overwrite
|
|
//
|
|
if (file_exists($name)) {
|
|
my_logger("skipping: $name already exists", $logger);
|
|
} elseif ($file->LOCAL) {
|
|
// Create the file
|
|
//
|
|
// for local file, just copy it
|
|
my_logger("creating: $name - copying LOCAL file $file->LOCAL", $logger);
|
|
if (!copy($file->LOCAL, $name)) {
|
|
$error = "unable to copy LOCAL file: $name";
|
|
@unlink($name);
|
|
return false;
|
|
}
|
|
} elseif ($file->INLINE) {
|
|
// for inline file, create with inline contents
|
|
my_logger("creating: $name - from INLINE content", $logger);
|
|
$contents = trim($file->INLINE).PHP_EOL;
|
|
if ($file->attributes()->Type == 'base64') {
|
|
my_logger("decoding: $name as base64", $logger);
|
|
$contents = base64_decode($contents);
|
|
if ($contents === false) {
|
|
$error = "unable to decode inline base64: $name";
|
|
return false;
|
|
}
|
|
}
|
|
if (!file_put_contents($name, $contents)) {
|
|
$error = "unable to create file: $name";
|
|
@unlink($name);
|
|
return false;
|
|
}
|
|
} elseif ($file->URL) {
|
|
// for download file, download and maybe verify the file MD5
|
|
my_logger("creating: $name - downloading from URL $file->URL", $logger);
|
|
if ( (download($file->URL, $name, $error) === false) && (download(filter_url($file->URL), $name, $error) === false) ) {
|
|
@unlink($name);
|
|
return false;
|
|
}
|
|
if ($file->SHA256) {
|
|
my_logger("checking: $name - SHA256", $logger);
|
|
if (hash_file('sha256', $name) != $file->SHA256) {
|
|
$error = "bad file SHA256: $name";
|
|
unlink($name);
|
|
return false;
|
|
}
|
|
} elseif ($file->MD5) {
|
|
my_logger("checking: $name - MD5", $logger);
|
|
if (md5_file($name) != $file->MD5) {
|
|
$error = "bad file MD5: $name";
|
|
unlink($name);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// Maybe change the file mode
|
|
//
|
|
if ($file->attributes()->Mode) {
|
|
// if file has 'Mode' attribute, apply it
|
|
$mode = $file->attributes()->Mode;
|
|
my_logger("setting: $name - mode to $mode", $logger);
|
|
if (!chmod($name, octdec($mode))) {
|
|
$error = "chmod failure: $name";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// Maybe "run" the file now
|
|
//
|
|
if ($file->attributes()->Run) {
|
|
$command = $file->attributes()->Run;
|
|
if ($download_only) {
|
|
$target = $name ?: ($file->LOCAL ?: 'inline script');
|
|
my_logger("skipping run: $command $target - download-only mode", $logger);
|
|
continue;
|
|
}
|
|
if ($name) {
|
|
my_logger("running: $command $name", $logger);
|
|
$retval = run("$command $name");
|
|
} elseif ($file->LOCAL) {
|
|
my_logger("running: $command $file->LOCAL", $logger);
|
|
$retval = run("$command $file->LOCAL");
|
|
} elseif ($file->INLINE) {
|
|
|
|
$name = '/tmp/inline'.$current_file.'-'.pathinfo($plugin_file, PATHINFO_FILENAME).'.sh';
|
|
file_put_contents($name, $file->INLINE);
|
|
$exec = $command." ".escapeshellarg($name);
|
|
my_logger("executing inline script: $exec", $logger);
|
|
$retval = run($exec);
|
|
unlink($name);
|
|
}
|
|
if ($retval != 0) {
|
|
$error = "run failed: '$command' returned $retval";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function move($src_file, $tar_dir) {
|
|
@mkdir($tar_dir);
|
|
return rename($src_file, $tar_dir."/".basename($src_file));
|
|
}
|
|
|
|
$notify = '/usr/local/emhttp/webGui/scripts/notify';
|
|
$boot = '/boot/config/plugins';
|
|
$plugins = '/var/log/plugins';
|
|
$tmp = '/tmp/plugins';
|
|
$download_only = false;
|
|
$script = $argv[0];
|
|
$method = $argv[1];
|
|
$optional_args = array_slice($argv, 2);
|
|
$builtin = ['unRAIDServer','unRAIDServer-'];
|
|
$nchan = in_array('nchan', $optional_args, true); // console or nchan output
|
|
|
|
// In following code,
|
|
// $plugin - is a basename of a plugin, eg, "myplugin.plg"
|
|
// $plugin_file - is an absolute path, eg, "/boot/config/plugins/myplugin.plg"
|
|
//
|
|
// MAIN - single argument
|
|
if ($argc < 2) {
|
|
write($usage);
|
|
done(1);
|
|
}
|
|
|
|
// plugin checkall
|
|
// check all installed plugins, except built-in
|
|
//
|
|
if ($method == 'checkall') {
|
|
if (!$cmd = realpath($script)) {
|
|
write("Unknown command: {$script}\n");
|
|
done(1);
|
|
}
|
|
foreach (glob("$plugins/*.plg", GLOB_NOSORT) as $link) {
|
|
// skip OS related plugins
|
|
if (in_array(basename($link,'.plg'),$builtin)) continue;
|
|
// only consider symlinks
|
|
$installed_plugin_file = @readlink($link);
|
|
if ($installed_plugin_file === false) continue;
|
|
if (plugin('pluginURL', $installed_plugin_file, $error) === false) continue;
|
|
$plugin = basename($installed_plugin_file);
|
|
write("plugin: checking $plugin ...\n");
|
|
exec("$cmd check $plugin >/dev/null");
|
|
}
|
|
write("plugin: checking finished.\n");
|
|
done(0);
|
|
}
|
|
|
|
// plugin updateall
|
|
// update all installed plugins, which have a update available
|
|
//
|
|
if ($method == 'updateall') {
|
|
if (!$cmd = realpath($script)) {
|
|
write("Unknown command: {$script}\n");
|
|
done(1);
|
|
}
|
|
foreach (glob("$plugins/*.plg", GLOB_NOSORT) as $link) {
|
|
// skip OS related plugins
|
|
if (in_array(basename($link,'.plg'),$builtin)) continue;
|
|
// only consider symlinks
|
|
$installed_plugin_file = @readlink($link);
|
|
if ($installed_plugin_file === false) continue;
|
|
if (plugin('pluginURL', $installed_plugin_file, $error) === false) continue;
|
|
$version = plugin('version', $installed_plugin_file, $error);
|
|
$plugin = basename($installed_plugin_file);
|
|
$latest = plugin('version', "$tmp/$plugin", $error);
|
|
// update only when newer
|
|
if (strcmp($latest,$version) > 0) {
|
|
write("plugin: updating $plugin ...\n");
|
|
exec("$cmd update $plugin >/dev/null");
|
|
}
|
|
}
|
|
write("plugin: updating finished.\n");
|
|
done(0);
|
|
}
|
|
|
|
// plugin checkos
|
|
// check built-in only
|
|
//
|
|
if ($method == 'checkos') {
|
|
if (!$cmd = realpath($script)) {
|
|
write("Unknown command: {$script}\n");
|
|
done(1);
|
|
}
|
|
foreach ($builtin as $link) {
|
|
// only consider symlinks
|
|
$installed_plugin_file = @readlink("$plugins/$link.plg");
|
|
if ($installed_plugin_file === false) continue;
|
|
if (plugin("pluginURL", $installed_plugin_file, $error) === false) continue;
|
|
$plugin = basename($installed_plugin_file);
|
|
write("plugin: checking $plugin ...\n");
|
|
exec("$cmd check $plugin >/dev/null");
|
|
}
|
|
write("plugin: checking finished.\n");
|
|
done(0);
|
|
}
|
|
|
|
// MAIN - two or three arguments
|
|
if ($argc < 3) {
|
|
write($usage);
|
|
done(1);
|
|
}
|
|
|
|
// plugin install [plugin_file] / plugin download [plugin_file]
|
|
// cases:
|
|
// a) dirname of [plugin_file] is /boot/config/plugins (system startup)
|
|
// b) [plugin_file] is a URL
|
|
// c) dirname of [plugin_file] is not /boot/config/plugins
|
|
//
|
|
$unraid = parse_ini_file('/etc/unraid-version');
|
|
if ($method == 'install' || $method == 'download') {
|
|
$argv[2] = preg_replace('#[\x00-\x1F\x80-\xFF]#', '', $argv[2]);
|
|
$plugin = basename($argv[2]);
|
|
$options = array_slice($argv, 3);
|
|
$download_only = ($method === 'download');
|
|
if (!$download_only) {
|
|
$legacy_download_flags = array_intersect($options, ['download-only', 'download']);
|
|
if (!empty($legacy_download_flags)) {
|
|
write("plugin: use 'plugin download' instead of 'plugin install ... download-only'\n");
|
|
done(1);
|
|
}
|
|
}
|
|
$forced = !empty($options);
|
|
if (pathinfo($plugin, PATHINFO_EXTENSION) != "plg") {
|
|
write("plugin: $plugin is not a plg file\n");
|
|
done(1);
|
|
}
|
|
if ($download_only) {
|
|
write("plugin: download-only mode enabled, skipping install commands\n");
|
|
}
|
|
$action = $download_only ? 'downloading' : 'installing';
|
|
write("plugin: $action: $plugin\n");
|
|
// check for URL
|
|
if (preg_match('#^https?://#',$argv[2])) {
|
|
$pluginURL = $argv[2];
|
|
// run hook scripts for pre processing
|
|
pre_hooks();
|
|
$plugin_file = "$tmp/$plugin";
|
|
write("plugin: downloading: $plugin\n");
|
|
if ( (download($pluginURL, $plugin_file, $error) === false) && (download(filter_url($pluginURL), $plugin_file, $error) === false) ) {
|
|
write("plugin: $error\n");
|
|
@unlink($plugin_file);
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
} else {
|
|
$plugin_file = realpath($argv[2]);
|
|
}
|
|
// bergware - check Unraid version dependency (if present)
|
|
$min = plugin('min', $plugin_file, $error);
|
|
if ($min && version_compare($unraid['version'], $min, '<')) {
|
|
write("plugin: installed Unraid version is too low, require at least version $min\n");
|
|
if (dirname($plugin_file) == "$boot") {
|
|
move($plugin_file, "$boot-error");
|
|
}
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
$max = plugin('max', $plugin_file, $error) ?: plugin('Unraid', $plugin_file, $error);
|
|
if ($max && version_compare($unraid['version'], $max, '>')) {
|
|
write("plugin: installed Unraid version is too high, require at most version $max\n");
|
|
if (dirname($plugin_file) == "$boot") {
|
|
move($plugin_file, "$boot-error");
|
|
}
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
$symlink = "$plugins/$plugin";
|
|
// check for re-install
|
|
$installed_plugin_file = @readlink($symlink);
|
|
if ($installed_plugin_file !== false) {
|
|
if ($plugin_file == $installed_plugin_file) {
|
|
write("plugin: not re-installing same plugin\n");
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
// must have version attributes for re-install
|
|
$version = plugin('version', $plugin_file, $error);
|
|
if ($version === false) {
|
|
write("plugin: $error\n");
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
$installed_version = plugin('version', $installed_plugin_file, $error);
|
|
if ($installed_version === false) {
|
|
write("plugin: $error\n");
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
// check version installation?
|
|
if (!$forced) {
|
|
// do not re-install if same plugin already installed or has higher version
|
|
if (strcmp($version, $installed_version) < 0) {
|
|
write("plugin: not installing older version\n");
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
if (strcmp($version, $installed_version) === 0) {
|
|
write("plugin: not reinstalling same version\n");
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
}
|
|
// run hook scripts for pre processing
|
|
pre_hooks();
|
|
if (plugin('install', $plugin_file, $error) === false) {
|
|
write("plugin: $error\n");
|
|
if (dirname($plugin_file) == "$boot") {
|
|
move($plugin_file, "$boot-error");
|
|
}
|
|
$event = "Install error";
|
|
$subject = "plugin: ".basename($plugin_file);
|
|
$description = "Plugin failed to install";
|
|
exec("$notify -e ".escapeshellarg($event)." -s ".escapeshellarg($subject)." -d ".escapeshellarg($description)." -i 'warning'");
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
// remove symlink for re-install
|
|
unlink($symlink);
|
|
} else {
|
|
// run hook scripts for pre processing
|
|
pre_hooks();
|
|
// fresh install
|
|
if (plugin('install', $plugin_file, $error) === false) {
|
|
write("plugin: $error\n");
|
|
if (dirname($plugin_file) == "$boot") {
|
|
move($plugin_file, "$boot-error");
|
|
}
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
}
|
|
// register successful install
|
|
$target = "$boot/$plugin";
|
|
if (!plugin('noInstall', $plugin_file, $error)) {
|
|
if ($target != $plugin_file) copy($plugin_file, $target);
|
|
symlink($target, $symlink);
|
|
$status = $download_only ? 'downloaded' : 'installed';
|
|
write("plugin: $plugin $status\n");
|
|
my_logger("$plugin $status", $logger);
|
|
} else {
|
|
$script_action = $download_only ? 'staged' : 'executed';
|
|
write("script: $plugin $script_action\n");
|
|
my_logger("script: $plugin $script_action", $logger);
|
|
}
|
|
// run hook scripts for post processing
|
|
post_hooks();
|
|
done(0);
|
|
}
|
|
|
|
// plugin check [plugin]
|
|
// We use the pluginURL attribute to download the latest plg file into the "/tmp/plugins/"
|
|
// directory.
|
|
//
|
|
if ($method == 'check') {
|
|
$plugin = $argv[2];
|
|
$symlink = "$plugins/$plugin";
|
|
write("plugin: checking: $plugin ...\n");
|
|
$installed_plugin_file = @readlink($symlink);
|
|
if ($installed_plugin_file === false) {
|
|
write("plugin: not installed\n");
|
|
// run hook scripts for post processing
|
|
post_hooks(1);
|
|
done(1);
|
|
}
|
|
$installed_pluginURL = plugin('pluginURL', $installed_plugin_file, $error);
|
|
if ($installed_pluginURL === false) {
|
|
write("plugin: $error\n");
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
$plugin_file = "$tmp/$plugin";
|
|
if ( (download($installed_pluginURL, $plugin_file, $error) === false) && (download(filter_url($installed_pluginURL), $plugin_file, $error) === false) ) {
|
|
write("plugin: $error\n");
|
|
@unlink($plugin_file);
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
// run hook scripts for pre processing
|
|
pre_hooks();
|
|
$version = plugin('version', $plugin_file, $error);
|
|
if ($version === false) {
|
|
write("plugin: $error\n");
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
write("$version\n");
|
|
// run hook scripts for post processing
|
|
post_hooks();
|
|
done(0);
|
|
}
|
|
|
|
// plugin update [plugin]
|
|
// [plugin] is the plg file we are going to be replacing, eg, "old.plg".
|
|
// We assume a "check" has already been done, ie, "/tmp/plugins/new.plg" already exists.
|
|
// We execute the "install" method of new.plg. If this fails, then we mark old.plg "not installed";
|
|
// the plugin manager will recognize this as an install error.
|
|
// If install new.plg succeeds, then we remove old.plg and copy new.plg in place.
|
|
// Finally we mark the new.plg "installed".
|
|
//
|
|
if ($method == 'update') {
|
|
$plugin = $argv[2];
|
|
$symlink = "$plugins/$plugin";
|
|
write("plugin: updating: $plugin\n");
|
|
$installed_plugin_file = @readlink($symlink);
|
|
if ($installed_plugin_file === false) {
|
|
write("plugin: $plugin not installed\n");
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
// verify previous check has been done
|
|
$plugin_file = "$tmp/$plugin";
|
|
if (!file_exists($plugin_file)) {
|
|
write("plugin: $plugin_file does not exist, check for updates first\n");
|
|
exit (1);
|
|
}
|
|
// bergware - check Unraid version dependency (if present)
|
|
$min = plugin('min', $plugin_file, $error);
|
|
if ($min && version_compare($unraid['version'], $min, '<')) {
|
|
write("plugin: installed Unraid version is too low, require at least version $min\n");
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
$max = plugin('max', $plugin_file, $error) ?: plugin('Unraid', $plugin_file, $error);
|
|
if ($max && version_compare($unraid['version'], $max, '>')) {
|
|
write("plugin: installed Unraid version is too high, require at most version $max\n");
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
// check for a reinstall of same version
|
|
if (strcmp(plugin('version', $installed_plugin_file, $error), plugin('version', $plugin_file, $error)) === 0) {
|
|
write("Not reinstalling same version\n");
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
// run hook scripts for pre processing
|
|
pre_hooks();
|
|
// install the updated plugin
|
|
if (plugin('install', $plugin_file, $error) === false) {
|
|
write("plugin: $error\n");
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
// install was successful, save the updated plugin so it installs again next boot
|
|
unlink($symlink);
|
|
$target = "$boot/$plugin";
|
|
copy($plugin_file, $target);
|
|
symlink($target, $symlink);
|
|
write("plugin: $plugin updated\n");
|
|
my_logger("$plugin updated", $logger);
|
|
// run hook scripts for post processing
|
|
post_hooks();
|
|
done(0);
|
|
}
|
|
|
|
// plugin remove [plugin]
|
|
// only .plg files should have a remove method
|
|
//
|
|
if ($method == 'remove') {
|
|
$plugin = $argv[2];
|
|
$symlink = "$plugins/$plugin";
|
|
write("plugin: removing: $plugin\n");
|
|
$installed_plugin_file = @readlink($symlink);
|
|
if ($installed_plugin_file !== false) {
|
|
// remove the symlink
|
|
unlink($symlink);
|
|
@unlink("$tmp/$plugin");
|
|
// run hook scripts for pre processing
|
|
pre_hooks();
|
|
if (plugin('remove', $installed_plugin_file, $error) === false) {
|
|
// but if can't remove, restore the symlink
|
|
if (is_file($installed_plugin_file)) symlink($installed_plugin_file, $symlink);
|
|
write("plugin: $error\n");
|
|
// run hook scripts for post processing
|
|
post_hooks($error);
|
|
done(1);
|
|
}
|
|
}
|
|
// remove the plugin file
|
|
move($installed_plugin_file, "$boot-removed");
|
|
write("plugin: $plugin removed\n");
|
|
my_logger("$plugin removed", $logger);
|
|
exec("/usr/local/sbin/update_cron");
|
|
// run hook scripts for post processing
|
|
post_hooks();
|
|
done(0);
|
|
}
|
|
|
|
// return attribute
|
|
//
|
|
$plugin_file = $argv[2];
|
|
$value = plugin($method, $plugin_file, $error);
|
|
if ($value === false) {
|
|
write("plugin: $error\n");
|
|
done(1);
|
|
}
|
|
write(strip_tags(html_entity_decode($value))."\n");
|
|
done(0);
|
|
?>
|