#!/usr/bin/php -q
<?PHP
// Copyright 2005-2016, Lime Technology
// License: GPLv2 only
//
// Program updates made by Bergware International (April 2016)
$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.
  
  This command 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 exract 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

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.

system - OPTIONAL
  If present the plugin is considered a system plugin and is installed in '/boot/plugins'.
  User plugins get installed in '/boot/config/plugins', which is the default.

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;

// Download a file from a URL.
// Returns TRUE if success else FALSE and fills in error.
//
function download($URL, $name, &$error) {
  if ($file = popen("wget --no-cache --progress=dot -O $name $URL 2>&1", 'r')) {
    echo "plugin: downloading: $URL ...\r";
    $level = -1;
    while (!feof($file)) {
      if (preg_match("/\d+%/", fgets($file), $matches)) {
        $percentage = substr($matches[0],0,-1);
        if ($percentage > $level) {
          echo "plugin: downloading: $URL ... $percentage%\r";
          $level = $percentage;
        }
      }
    }
    if (($perror = pclose($file)) == 0) {
      echo "plugin: downloading: $URL ... done\n";
      return true;
    } else {
      echo "plugin: downloading: $URL ... failed (".error_desc($perror).")\n";
      $error = "wget: $URL download failure (".error_desc($perror).")";
      return false;
    }
  } else {
    $error = "wget: $URL failed to open";
    return false;
  }
}

// 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';
    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;
  }
}

// Deal with logging message.
//
function logger($message) {
//  echo "$message\n";
  shell_exec("logger $message");
}

// 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;
  $methods = ["install", "remove"];

  // parse plugin definition XML file
  $xml = simplexml_load_file($plugin_file, NULL, LIBXML_NOCDATA);
  if ($xml === false) {
    $error = "xml parse error";
    return false;
  }

  // dump
  if ($method == "dump") {
    // echo $xml->asXML();
    echo print_r($xml);
    return true;
  }

  // release notes
  if ($method == 'changes') {
    if (!$xml->CHANGES) return false;
    return trim($xml->CHANGES);
  }

  // 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
  //
  foreach ($xml->FILE as $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,'<')) {
      echo "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,'>')) {
      echo "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 = "plugin: error: unable to create parent directory for $name";
          return false;
        }
      }
      // If file already exists, do not overwrite
      //
      if (file_exists($name)) {
        logger("plugin: skipping: $name already exists");
      } elseif ($file->LOCAL) {
        // Create the file
        //
        // for local file, just copy it
        logger("plugin: creating: $name - copying LOCAL file $file->LOCAL");
        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
        logger("plugin: creating: $name - from INLINE content");
        $contents = trim($file->INLINE).PHP_EOL;
        if ($file->attributes()->Type == 'base64') {
          logger("plugin: decoding: $name as base64");
          $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
        logger("plugin: creating: $name - downloading from URL $file->URL");
        if (download($file->URL, $name, $error) === false) {
          @unlink($name);
          return false;
        }
        if ($file->MD5) {
          logger("plugin: checking: $name - MD5");
          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;
        logger("plugin: setting: $name - mode to $mode");
        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 ($name) {
        logger("plugin: running: $name");
        system("$command $name", $retval);
      } elseif ($file->LOCAL) {
        logger("plugin: running: $file->LOCAL");
        system("$command $file->LOCAL", $retval);
      } elseif ($file->INLINE) {
        logger("plugin: running: 'anonymous'");
        $inline = escapeshellarg($file->INLINE);
        passthru("echo $inline | $command", $retval);
      }
      if ($retval) {
	      $error = "run failed: $command retval: $retval";
	      return false;
      }
    }
  }
  return true;
}

function move($src_file, $tar_dir) {
  @mkdir($tar_dir);
  return rename($src_file, $tar_dir."/".basename($src_file));
}

// 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"
//
if ($argc < 2) {
  echo $usage;
  exit(1);
}
$method = $argv[1];
$builtin = ['unRAIDServer'];

// plugin checkall
// check all installed plugins, except built-in
//
if ($method == "checkall") {
  foreach (glob("/var/log/plugins/*", GLOB_NOSORT) as $link) {
    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);
    echo "plugin: checking $plugin ...\n";
    exec(realpath($argv[0]) . " check $plugin", $output, $retval);
  }
  exit(0);
}

// plugin checkos
// check built-in only
//
if ($method == "checkos") {
  foreach ($builtin as $link) {
    // only consider symlinks
    $installed_plugin_file = @readlink("/var/log/plugins/$link.plg");
    if ($installed_plugin_file === false) continue;
    if (plugin("pluginURL", $installed_plugin_file, $error) === false) continue;
    $plugin = basename($installed_plugin_file);
    echo "plugin: checking $plugin ...\n";
    exec(realpath($argv[0]) . " check $plugin", $output, $retval);
  }
  exit(0);
}

if ($argc < 3) {
  echo $usage;
  exit(1);
}

// plugin install [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") {
  echo "plugin: installing: $argv[2]\n";
  // check for URL
  if ((strpos($argv[2], "http://") === 0) || (strpos($argv[2], "https://") === 0)) {
    $pluginURL = $argv[2];
    echo "plugin: downloading $pluginURL\n";
    $plugin_file = "/tmp/plugins/".basename($pluginURL);
    if (!download($pluginURL, $plugin_file, $error)) {
      echo "plugin: $error\n";
      @unlink($plugin_file);
      exit(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,'<')) {
    echo "plugin: installed unRAID version is too low, require at least version $min\n";
    exit(1);
  }
  $max = plugin("max", $plugin_file, $error);
  if (empty($max))
    $max = plugin("unRAID", $plugin_file, $error);
  if ($max && version_compare($unraid['version'],$max,'>')) {
    echo "plugin: installed unRAID version is too high, require at most version $max\n";
    exit(1);
  }
  $plugin = basename($plugin_file);
  // check for re-install
  $installed_plugin_file = @readlink("/var/log/plugins/$plugin");
  if ($installed_plugin_file !== false) {
    if ($plugin_file == $installed_plugin_file) {
      echo "plugin: not re-installing same plugin\n";
      exit(1);
    }
    // must have version attributes for re-install
    $version = plugin("version", $plugin_file, $error);
    if ($version === false) {
      echo "plugin: $error\n";
      exit(1);
    }
    $installed_version = plugin("version", $installed_plugin_file, $error);
    if ($installed_version === false) {
      echo "plugin: $error\n";
      exit(1);
    }
    // check version installation?
    $forced = $argc==4 ? $argv[3] : false;
    if (!$forced) {
      // do not re-install if same plugin already installed or has higher version
      if (strcmp($version, $installed_version) < 0) {
        echo "plugin: not installing older version\n";
        exit(1);
      }
      if (strcmp($version, $installed_version) == 0) {
        echo "plugin: not reinstalling same version\n";
        exit(1);
      }
    }
    if (plugin("install", $plugin_file, $error) === false) {
      echo "plugin: $error\n";
      if (dirname($plugin_file) == "/boot/config/plugins") {
        move($plugin_file, "/boot/config/plugins-error");
      }
      exit(1);
    }
    unlink("/var/log/plugins/$plugin");
  } else {
    // fresh install
    if (plugin("install", $plugin_file, $error) === false) {
      echo "plugin: $error\n";
      if (dirname($plugin_file) == "/boot/config/plugins") {
        move($plugin_file, "/boot/config/plugins-error");
      }
      exit(1);
    }
  }
  // register successful install
  // Bergware change: add user or system plugin selection - deprecated
  $plugintype = plugin("plugintype", $plugin_file, $error);
  $target = $plugintype != "system" ? "/boot/config/plugins/$plugin" : "/boot/plugins/$plugin";
  if ($target != $plugin_file) copy($plugin_file, $target);
  symlink($target, "/var/log/plugins/$plugin");
  echo "plugin: installed\n";
  exit(0);
}

// plugin remove [plugin]
// only .plg files should have a remove method
//
if ($method == "remove") {
  echo "plugin: removing: {$argv[2]}\n";
  $plugin = $argv[2];
  $installed_plugin_file = @readlink("/var/log/plugins/$plugin");
  if ($installed_plugin_file !== false) {
    // remove the symlink
    unlink("/var/log/plugins/$plugin");
    @unlink("/tmp/plugins/$plugin");
    if (plugin("remove", $installed_plugin_file, $error) === false) {
      // but if can't remove, restore the symlink
      symlink($installed_plugin_file, "/var/log/plugins/$plugin");
      echo "plugin: $error\n";
      exit(1);
    }
  }
  // remove the plugin file
  move($installed_plugin_file, "/boot/config/plugins-removed");
  echo "plugin: removed\n";
  exit(0);
}

// plugin check [plugin]
// We use the pluginURL attribute to download the latest plg file into the "/tmp/plugins/"
// directory.
//
if ($method == "check") {
  echo "plugin: checking: {$argv[2]}\n";
  $plugin = $argv[2];
  $installed_plugin_file = @readlink("/var/log/plugins/$plugin");
  if ($installed_plugin_file === false) {
    echo "plugin: not installed\n";
    exit(1);
  }
  $installed_pluginURL = plugin("pluginURL", $installed_plugin_file, $error);
  if ($installed_pluginURL === false) {
    echo "plugin: $error\n";
    exit(1);
  }
  $plugin_file = "/tmp/plugins/$plugin";
  if (!download($installed_pluginURL, $plugin_file, $error)) {
    echo "plugin: $error\n";
    @unlink($plugin_file);
    exit(1);
  }
  $version = plugin("version", $plugin_file, $error);
  if ($version === false) {
    echo "plugin: $error\n";
    exit(1);
  }
  echo "$version\n";
  exit(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") {
  echo "plugin: updating: {$argv[2]}\n";
  $plugin = $argv[2];
  $installed_plugin_file = @readlink("/var/log/plugins/$plugin");
  if ($installed_plugin_file === false) {
    echo "plugin: not installed\n";
    exit(1);
  }
  // verify previous check has been done
  $plugin_file = "/tmp/plugins/$plugin";
  if (!file_exists($plugin_file)) {
    echo "plugin: $plugin_file does not exist, check first\n";
    exit (1);
  }
  // bergware - check unRAID version dependency (if present)
  $min = plugin("min", $plugin_file, $error);
  if ($min && version_compare($unraid['version'],$min,'<')) {
    echo "plugin: installed unRAID version is too low, require at least version $min\n";
    exit(1);
  }
  $max = plugin("max", $plugin_file, $error);
  if (empty($max))
    $max = plugin("unRAID", $plugin_file, $error);
  if ($max && version_compare($unraid['version'],$max,'>')) {
    echo "plugin: installed unRAID version is too high, require at most version $max\n";
    exit(1);
  }
  // install the updated plugin
  if (plugin("install", $plugin_file, $error) === false) {
    echo "plugin: $error\n";
    exit(1);
  }
  // install was successful, save the updated plugin so it installs again next boot
  unlink("/var/log/plugins/$plugin");
  copy($plugin_file, "/boot/config/plugins/$plugin");
  symlink("/boot/config/plugins/$plugin", "/var/log/plugins/$plugin");
  echo "plugin: updated\n";
  exit(0);
}

// <attribute>
//
$plugin_file = $argv[2];
$value = plugin($method, $plugin_file, $error);
if ($value === false) {
  echo "plugin: $error\n";
  exit(1);
}
echo "$value\n";
exit(0);
?>
