New plugin manager half-implemented. Most of the UI/frontend code is done. Moved sql_parse.php to /includes/ to allow use after installation - TODO: check installer, etc. for breakage
authorDan
Wed, 09 Apr 2008 19:27:02 -0400
changeset 526 b2fb50d572c7
parent 525 3f2dfdb99be4
child 527 21e11f564463
New plugin manager half-implemented. Most of the UI/frontend code is done. Moved sql_parse.php to /includes/ to allow use after installation - TODO: check installer, etc. for breakage
includes/clientside/css/enano-shared.css
includes/clientside/static/ajax.js
includes/constants.php
includes/sql_parse.php
includes/template.php
install/includes/sql_parse.php
install/includes/stages/database_post.php
install/includes/stages/finish.php
install/includes/stages/install.php
install/upgrade.php
language/english/admin.json
plugins/admin/PluginManager.php
--- a/includes/clientside/css/enano-shared.css	Tue Apr 08 20:32:30 2008 -0400
+++ b/includes/clientside/css/enano-shared.css	Wed Apr 09 19:27:02 2008 -0400
@@ -783,3 +783,23 @@
   position: relative;
   top: 10px;
 }
+
+/* pseudo-buttons made with <a> tags */
+.abutton {
+  padding: 3px 5px;
+  background-color: #f0f0f0;
+  cursor: pointer;
+  margin: 0 3px;
+}
+
+.abutton:hover {
+  color: #f0f0f0 !important;
+}
+
+.abutton_green       { color: #00aa00 !important; }
+.abutton_green:hover { background-color: #00aa00; }
+.abutton_blue        { color: #0000aa !important; }
+.abutton_blue:hover  { background-color: #0000aa; }
+.abutton_red         { color: #aa0000 !important; }
+.abutton_red:hover   { background-color: #aa0000; }
+
--- a/includes/clientside/static/ajax.js	Tue Apr 08 20:32:30 2008 -0400
+++ b/includes/clientside/static/ajax.js	Wed Apr 09 19:27:02 2008 -0400
@@ -1517,3 +1517,111 @@
     });
 }
 
+function ajaxPluginAction(action, plugin_filename, btnobj)
+{
+  // if installing or uninstalling, confirm
+  if ( action == 'install' || action == 'uninstall' )
+  {
+    var prompt = miniPrompt(function(div)
+      {
+        var txtholder = document.createElement('div');
+        txtholder.style.textAlign = 'center';
+        txtholder.appendChild(document.createTextNode($lang.get('acppl_msg_confirm_' + action)));
+        txtholder.appendChild(document.createElement('br'));
+        txtholder.appendChild(document.createElement('br'));
+        
+        // create buttons
+        var btn_go = document.createElement('a');
+        btn_go.className = 'abutton abutton_red';
+        btn_go.href = '#';
+        btn_go._action = action;
+        btn_go._filename = plugin_filename;
+        btn_go._button = btnobj;
+        btn_go.appendChild(document.createTextNode($lang.get('acppl_btn_' + action)));
+        btn_go.style.fontWeight = 'bold';
+        txtholder.appendChild(btn_go);
+        
+        // space
+        txtholder.appendChild(document.createTextNode(' '));
+        
+        // cancel
+        var btn_cancel = document.createElement('a');
+        btn_cancel.className = 'abutton abutton_blue';
+        btn_cancel.href = '#';
+        btn_cancel.appendChild(document.createTextNode($lang.get('etc_cancel')));
+        
+        txtholder.appendChild(btn_cancel);
+        div.appendChild(txtholder);
+        
+        btn_go.onclick = function()
+        {
+          ajaxPluginAction(this._action + '_confirm', this._filename, this._button);
+          miniPromptDestroy(this);
+          return false;
+        }
+        btn_cancel.onclick = function()
+        {
+          miniPromptDestroy(this);
+          return false;
+        }
+      });
+    return true;
+  }
+  action = action.replace(/_confirm$/, '');
+  var request = toJSONString({
+      mode: action,
+      plugin: plugin_filename
+    });
+  ajaxPost(makeUrlNS('Admin', 'PluginManager/action.json'), 'r=' + ajaxEscape(request), function()
+    {
+      if ( ajax.readyState == 4 && ajax.status == 200 )
+      {
+        if ( ajax.responseText == 'good' )
+        {
+          ajaxPage( namespace_list['Admin'] + 'PluginManager' );
+        }
+        else
+        {
+          var response = String(ajax.responseText + '');
+          if ( response.substr(0, 1) != '{' )
+          {
+            handle_invalid_json(response);
+            return false;
+          }
+          response = parseJSON(response);
+          if ( response.mode != 'error' )
+          {
+            console.debug(response);
+            return false;
+          }
+          // wait for fade effect to finish its run
+          setTimeout(function()
+            {
+              miniPrompt(function(div)
+                {
+                  var txtholder = document.createElement('div');
+                  txtholder.style.textAlign = 'center';
+                  txtholder.appendChild(document.createTextNode(response.error));
+                  txtholder.appendChild(document.createElement('br'));
+                  txtholder.appendChild(document.createElement('br'));
+                  
+                  // close button
+                  var btn_cancel = document.createElement('a');
+                  btn_cancel.className = 'abutton abutton_red';
+                  btn_cancel.href = '#';
+                  btn_cancel.appendChild(document.createTextNode($lang.get('etc_ok')));
+                  
+                  txtholder.appendChild(btn_cancel);
+                  div.appendChild(txtholder);
+                  
+                  btn_cancel.onclick = function()
+                  {
+                    miniPromptDestroy(this);
+                    return false;
+                  }
+                });
+            }, 750);
+        }
+      }
+    });
+}
--- a/includes/constants.php	Tue Apr 08 20:32:30 2008 -0400
+++ b/includes/constants.php	Wed Apr 09 19:27:02 2008 -0400
@@ -87,6 +87,11 @@
 define('PM_TRASH', 32);
 define('PM_DELIVERED', 64);
 
+// Plugin status
+define('PLUGIN_INSTALLED', 1);
+define('PLUGIN_DISABLED', 2);
+define('PLUGIN_OUTOFDATE', 4);
+
 // Other stuff
 
 define('MAX_PMS_PER_BATCH', 7); // The maximum number of users that users can send PMs to in one go; restriction does not apply to users with mod_misc rights
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/includes/sql_parse.php	Wed Apr 09 19:27:02 2008 -0400
@@ -0,0 +1,149 @@
+<?php
+
+/*
+ * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
+ * Version 1.1.1
+ * Copyright (C) 2006-2007 Dan Fuhry
+ * Installation package
+ * sql_parse.php - SQL query splitter and templater
+ *
+ * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ */
+
+/**
+ * Parses a full file of SQL into individual queries. Also includes substitution (template) functions.
+ * @package Enano
+ * @subpackage Installer
+ * @author Dan Fuhry
+ */
+
+class SQL_Parser
+{
+  /**
+   * The SQL to be parsed.
+   * @var string
+   * @access private
+   */
+  
+  private $sql_string;
+  
+  /**
+   * Parsed SQL array
+   * @var array
+   * @access private
+   */
+  
+  private $sql_array;
+  
+  /**
+   * Template variables.
+   * @var array
+   * @access private
+   */
+  
+  private $tpl_strings;
+  
+  /**
+   * Constructor.
+   * @param string If this contains newlines, it will be treated as the target SQL. If not, will be treated as a filename.
+   */
+  
+  public function __construct($sql)
+  {
+    if ( strpos($sql, "\n") )
+    {
+      $this->sql_string = $sql;
+    }
+    else
+    {
+      if ( file_exists($sql) )
+      {
+        $this->sql_string = @file_get_contents($sql);
+        if ( empty($this->sql_string) )
+        {
+          throw new Exception('SQL file is blank or permissions are bad');
+        }
+      }
+      else
+      {
+        throw new Exception('SQL file doesn\'t exist');
+      }
+    }
+    $this->sql_array = false;
+    $this->tpl_strings = array();
+  }
+  
+  /**
+   * Sets template variables.
+   * @param array Associative array of template variables to assign
+   */
+  
+  public function assign_vars($vars)
+  {
+    if ( !is_array($vars) )
+      return false;
+    $this->tpl_strings = array_merge($this->tpl_strings, $vars);
+  }
+  
+  /**
+   * Internal function to parse the SQL.
+   * @access private
+   */
+  
+  private function parse_sql()
+  {
+    $this->sql_array = $this->sql_string;
+    foreach ( $this->tpl_strings as $key => $value )
+    {
+      $this->sql_array = str_replace("{{{$key}}}", $value, $this->sql_array);
+    }
+    
+    // Build an array of queries
+    $this->sql_array = explode("\n", $this->sql_array);
+    
+    foreach ( $this->sql_array as $i => $sql )
+    {
+      $query =& $this->sql_array[$i];
+      $t = trim($query);
+      if ( empty($t) || preg_match('/^(\#|--)/i', $t) )
+      {
+        unset($this->sql_array[$i]);
+        unset($query);
+      }
+    }
+    unset($query);
+    
+    $this->sql_array = array_values($this->sql_array);
+    $this->sql_array = implode("\n", $this->sql_array);
+    $this->sql_array = explode(";\n", $this->sql_array);
+    
+    foreach ( $this->sql_array as $i => $sql )
+    {
+      $query =& $this->sql_array[$i];
+      if ( substr($query, ( strlen($query) - 1 ), 1 ) != ';' )
+      {
+        $query .= ';';
+      }
+    }
+    unset($query);
+  }
+  
+  /**
+   * Returns the parsed array of SQL queries.
+   * @param bool Optional. Defaults to false. If true, a parse is performed even if it already happened.
+   * @return array
+   */
+  
+  public function parse($force_reparse = false)
+  {
+    if ( !$this->sql_array || $force_reparse )
+      $this->parse_sql();
+    return $this->sql_array;
+  }
+}
+
+?>
--- a/includes/template.php	Tue Apr 08 20:32:30 2008 -0400
+++ b/includes/template.php	Wed Apr 09 19:27:02 2008 -0400
@@ -1017,6 +1017,12 @@
       $this->load_theme($session->theme, $session->style);
     }
     
+    // I feel awful doing this.
+    if ( preg_match('/^W3C_Validator/', @$_SERVER['HTTP_USER_AGENT']) )
+    {
+      header('Content-type: application/xhtml+xml');
+    }
+    
     $headers_sent = true;
     if(!defined('ENANO_HEADERS_SENT'))
       define('ENANO_HEADERS_SENT', '');
--- a/install/includes/sql_parse.php	Tue Apr 08 20:32:30 2008 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-<?php
-
-/*
- * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.1.1
- * Copyright (C) 2006-2007 Dan Fuhry
- * Installation package
- * sql_parse.php - SQL query splitter and templater
- *
- * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
- * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
- */
-
-/**
- * Parses a full file of SQL into individual queries. Also includes substitution (template) functions.
- * @package Enano
- * @subpackage Installer
- * @author Dan Fuhry
- */
-
-class SQL_Parser
-{
-  /**
-   * The SQL to be parsed.
-   * @var string
-   * @access private
-   */
-  
-  private $sql_string;
-  
-  /**
-   * Parsed SQL array
-   * @var array
-   * @access private
-   */
-  
-  private $sql_array;
-  
-  /**
-   * Template variables.
-   * @var array
-   * @access private
-   */
-  
-  private $tpl_strings;
-  
-  /**
-   * Constructor.
-   * @param string If this contains newlines, it will be treated as the target SQL. If not, will be treated as a filename.
-   */
-  
-  public function __construct($sql)
-  {
-    if ( strpos($sql, "\n") )
-    {
-      $this->sql_string = $sql;
-    }
-    else
-    {
-      if ( file_exists($sql) )
-      {
-        $this->sql_string = @file_get_contents($sql);
-        if ( empty($this->sql_string) )
-        {
-          throw new Exception('SQL file is blank or permissions are bad');
-        }
-      }
-      else
-      {
-        throw new Exception('SQL file doesn\'t exist');
-      }
-    }
-    $this->sql_array = false;
-    $this->tpl_strings = array();
-  }
-  
-  /**
-   * Sets template variables.
-   * @param array Associative array of template variables to assign
-   */
-  
-  public function assign_vars($vars)
-  {
-    if ( !is_array($vars) )
-      return false;
-    $this->tpl_strings = array_merge($this->tpl_strings, $vars);
-  }
-  
-  /**
-   * Internal function to parse the SQL.
-   * @access private
-   */
-  
-  private function parse_sql()
-  {
-    $this->sql_array = $this->sql_string;
-    foreach ( $this->tpl_strings as $key => $value )
-    {
-      $this->sql_array = str_replace("{{{$key}}}", $value, $this->sql_array);
-    }
-    
-    // Build an array of queries
-    $this->sql_array = explode("\n", $this->sql_array);
-    
-    foreach ( $this->sql_array as $i => $sql )
-    {
-      $query =& $this->sql_array[$i];
-      $t = trim($query);
-      if ( empty($t) || preg_match('/^(\#|--)/i', $t) )
-      {
-        unset($this->sql_array[$i]);
-        unset($query);
-      }
-    }
-    unset($query);
-    
-    $this->sql_array = array_values($this->sql_array);
-    $this->sql_array = implode("\n", $this->sql_array);
-    $this->sql_array = explode(";\n", $this->sql_array);
-    
-    foreach ( $this->sql_array as $i => $sql )
-    {
-      $query =& $this->sql_array[$i];
-      if ( substr($query, ( strlen($query) - 1 ), 1 ) != ';' )
-      {
-        $query .= ';';
-      }
-    }
-    unset($query);
-  }
-  
-  /**
-   * Returns the parsed array of SQL queries.
-   * @param bool Optional. Defaults to false. If true, a parse is performed even if it already happened.
-   * @return array
-   */
-  
-  public function parse($force_reparse = false)
-  {
-    if ( !$this->sql_array || $force_reparse )
-      $this->parse_sql();
-    return $this->sql_array;
-  }
-}
-
-?>
--- a/install/includes/stages/database_post.php	Tue Apr 08 20:32:30 2008 -0400
+++ b/install/includes/stages/database_post.php	Wed Apr 09 19:27:02 2008 -0400
@@ -19,7 +19,7 @@
 
 // Start up the DBAL
 require( ENANO_ROOT . '/includes/dbal.php' );
-require( ENANO_ROOT . '/install/includes/sql_parse.php' );
+require( ENANO_ROOT . '/includes/sql_parse.php' );
 $dbal = new $driver();
 $db_host =& $_POST['db_host'];
 $db_user =& $_POST['db_user'];
--- a/install/includes/stages/finish.php	Tue Apr 08 20:32:30 2008 -0400
+++ b/install/includes/stages/finish.php	Wed Apr 09 19:27:02 2008 -0400
@@ -18,7 +18,7 @@
   die();
 
 require ( ENANO_ROOT . '/install/includes/libenanoinstall.php' );
-require ( ENANO_ROOT . '/install/includes/sql_parse.php' );
+require ( ENANO_ROOT . '/includes/sql_parse.php' );
 require ( ENANO_ROOT . '/includes/common.php' );
 
 if ( !in_array($dbdriver, $supported_drivers) )
--- a/install/includes/stages/install.php	Tue Apr 08 20:32:30 2008 -0400
+++ b/install/includes/stages/install.php	Wed Apr 09 19:27:02 2008 -0400
@@ -18,7 +18,7 @@
   die();
 
 require ( ENANO_ROOT . '/install/includes/libenanoinstall.php' );
-require ( ENANO_ROOT . '/install/includes/sql_parse.php' );
+require ( ENANO_ROOT . '/includes/sql_parse.php' );
 require ( ENANO_ROOT . '/includes/dbal.php' );
 require ( ENANO_ROOT . '/config.new.php' );
 
--- a/install/upgrade.php	Tue Apr 08 20:32:30 2008 -0400
+++ b/install/upgrade.php	Wed Apr 09 19:27:02 2008 -0400
@@ -28,7 +28,7 @@
 @ini_set('display_errors', 'on');
 
 // Load installer files
-require_once('includes/sql_parse.php');
+require_once('../includes/sql_parse.php');
 require_once('includes/common.php');
 require_once('includes/libenanoinstall.php');
 
--- a/language/english/admin.json	Tue Apr 08 20:32:30 2008 -0400
+++ b/language/english/admin.json	Wed Apr 09 19:27:02 2008 -0400
@@ -382,7 +382,26 @@
       msg_demo_mode: 'Hmm, enabling executables, are we? Tsk tsk. I\'d love to know what\'s in that EXE file you want to upload. OK, maybe you didn\'t enable EXEs. But nevertheless, changing allowed filetypes is disabled in the demo.',
     },
     acppl: {
+      lbl_plugin_name: '<b>%plugin%</b> by %author%',
+      lbl_status_installed: 'Installed',
+      lbl_status_uninstalled: 'Not installed',
+      lbl_status_system: 'System plugin',
+      lbl_status_need_upgrade: 'Disabled (needs upgrade)',
+      lbl_status_disabled: 'Disabled',
+      lbl_filename: 'Filename:',
+      lbl_plugin_site: 'Plugin homepage:',
+      lbl_author_site: 'Author homepage:',
+      lbl_version: 'Version:',
+      lbl_installed_version: 'Installed version:',
       
+      btn_install: 'Install',
+      btn_disable: 'Disable',
+      btn_enable: 'Enable',
+      btn_upgrade: 'Upgrade',
+      btn_uninstall: 'Uninstall',
+      
+      msg_confirm_uninstall: 'Please confirm that you want to uninstall this plugin and that it doesn\'t provide any shared functions that other plugins depend on.',
+      msg_confirm_install: 'Plugins are not supported by the Enano project and could harm your site if malicious. You should only install plugins from sources that you trust.',
     },
     acppm: {
       heading_main: 'Edit page properties',
--- a/plugins/admin/PluginManager.php	Tue Apr 08 20:32:30 2008 -0400
+++ b/plugins/admin/PluginManager.php	Wed Apr 09 19:27:02 2008 -0400
@@ -255,9 +255,160 @@
       }
       // decide if it's a system plugin
       $plugin_meta['system plugin'] = in_array($dh, $plugins->system_plugins);
+      // reset installed variable
+      $plugin_meta['installed'] = false;
+      $plugin_meta['status'] = 0;
       // all checks passed
       $plugin_list[$dh] = $plugin_meta;
     }
   }
-  echo '<pre>' . print_r($plugin_list, true) . '</pre>';
+  // gather info about installed plugins
+  $q = $db->sql_query('SELECT plugin_filename, plugin_version, plugin_flags FROM ' . table_prefix . 'plugins;');
+  if ( !$q )
+    $db->_die();
+  while ( $row = $db->fetchrow() )
+  {
+    if ( !isset($plugin_list[ $row['plugin_filename'] ]) )
+    {
+      // missing plugin file, don't report (for now)
+      continue;
+    }
+    $filename =& $row['plugin_filename'];
+    $plugin_list[$filename]['installed'] = true;
+    $plugin_list[$filename]['status'] = PLUGIN_INSTALLED;
+    if ( $row['plugin_version'] != $plugin_list[$filename]['version'] )
+    {
+      $plugin_list[$filename]['status'] |= PLUGIN_OUTOFDATE;
+      $plugin_list[$filename]['version installed'] = $row['plugin_version'];
+    }
+    if ( $row['plugin_flags'] & PLUGIN_DISABLED )
+    {
+      $plugin_list[$filename]['status'] |= PLUGIN_DISABLED;
+    }
+  }
+  $db->free_result();
+  
+  // sort it all out by filename
+  ksort($plugin_list);
+  
+  // start printing things out
+  acp_start_form();
+  ?>
+  <div class="tblholder">
+    <table border="0" cellspacing="1" cellpadding="5">
+      <?php
+      $rowid = '2';
+      foreach ( $plugin_list as $filename => $data )
+      {
+        // print out all plugins
+        $rowid = ( $rowid == '1' ) ? '2' : '1';
+        $plugin_name = ( preg_match('/^[a-z0-9_]+$/', $data['plugin name']) ) ? $lang->get($data['plugin name']) : $data['plugin name'];
+        $plugin_basics = $lang->get('acppl_lbl_plugin_name', array(
+            'plugin' => $plugin_name,
+            'author' => $data['author']
+          ));
+        $color = '';
+        $buttons = '';
+        if ( $data['system plugin'] )
+        {
+          $status = $lang->get('acppl_lbl_status_system');
+        }
+        else if ( $data['installed'] && !( $data['status'] & PLUGIN_DISABLED ) && !( $data['status'] & PLUGIN_OUTOFDATE ) )
+        {
+          // this plugin is all good
+          $color = '_green';
+          $status = $lang->get('acppl_lbl_status_installed');
+          $buttons = 'uninstall|disable';
+        }
+        else if ( $data['installed'] && $data['status'] & PLUGIN_OUTOFDATE )
+        {
+          $color = '_red';
+          $status = $lang->get('acppl_lbl_status_need_upgrade');
+          $buttons = 'uninstall|update';
+        }
+        else if ( $data['installed'] && $data['status'] & PLUGIN_DISABLED )
+        {
+          $color = '_red';
+          $status = $lang->get('acppl_lbl_status_disabled');
+          $buttons = 'uninstall|enable';
+        }
+        else
+        {
+          $color = '_red';
+          $status = $lang->get('acppl_lbl_status_uninstalled');
+          $buttons = 'install';
+        }
+        $uuid = md5($data['plugin name'] . $data['version'] . $filename);
+        $desc = ( preg_match('/^[a-z0-9_]+$/', $data['description']) ) ? $lang->get($data['description']) : $data['description'];
+        $desc = sanitize_html($desc);
+        
+        $additional = '';
+        
+        // filename
+        $additional .= '<b>' . $lang->get('acppl_lbl_filename') . '</b> ' . "{$filename}<br />";
+        
+        // plugin's site
+        $data['plugin uri'] = htmlspecialchars($data['plugin uri']);
+        $additional .= '<b>' . $lang->get('acppl_lbl_plugin_site') . '</b> ' . "<a href=\"{$data['plugin uri']}\">{$data['plugin uri']}</a><br />";
+        
+        // author's site
+        $data['author uri'] = htmlspecialchars($data['author uri']);
+        $additional .= '<b>' . $lang->get('acppl_lbl_author_site') . '</b> ' . "<a href=\"{$data['author uri']}\">{$data['author uri']}</a><br />";
+        
+        // version
+        $additional .= '<b>' . $lang->get('acppl_lbl_version') . '</b> ' . "{$data['version']}<br />";
+        
+        // installed version
+        if ( $data['status'] & PLUGIN_OUTOFDATE )
+        {
+          $additional .= '<b>' . $lang->get('acppl_lbl_installed_version') . '</b> ' . "{$data['version installed']}<br />";
+        }
+        
+        // build list of buttons
+        $buttons_html = '';
+        if ( !empty($buttons) )
+        {
+          $filename_js = addslashes($filename);
+          $buttons = explode('|', $buttons);
+          $colors = array(
+              'install' => 'green',
+              'disable' => 'blue',
+              'enable' => 'blue',
+              'upgrade' => 'green',
+              'uninstall' => 'red'
+            );
+          foreach ( $buttons as $button )
+          {
+            $btnface = $lang->get("acppl_btn_$button");
+            $buttons_html .= "<a href=\"#\" onclick=\"ajaxPluginAction('$button', '$filename_js', this); return false;\" class=\"abutton_{$colors[$button]} abutton\">$btnface</a>\n";
+          }
+        }
+        
+        echo "<tr>
+                <td class=\"row{$rowid}$color\">
+                  <div style=\"float: right;\">
+                    <b>$status</b>
+                  </div>
+                  <div style=\"cursor: pointer;\" onclick=\"if ( !this.fx ) this.fx = new Spry.Effect.Blind('plugininfo_$uuid', { duration: 500, from: '0%', to: '100%', toggle: true }); this.fx.start();\"
+                    $plugin_basics
+                  </div>
+                  <span class=\"menuclear\"></span>
+                  <div id=\"plugininfo_$uuid\" style=\"display: none;\">
+                    $desc
+                    <div style=\"padding: 5px;\">
+                      $additional
+                      <div style=\"float: right; position: relative; top: -10px;\">
+                        $buttons_html
+                      </div>
+                      <span class=\"menuclear\"></span>
+                    </div>
+                  </div>
+                </td>
+              </tr>";
+      }
+      ?>
+    </table>
+  </div>
+  <?php
+  echo '</form>';
 }