(Hopefully) finished new plugin manager and implemented the utilization of it. Still HIGHLY experimental.
authorDan
Wed, 09 Apr 2008 22:37:37 -0400
changeset 527 21e11f564463
parent 526 b2fb50d572c7
child 528 43535769970b
(Hopefully) finished new plugin manager and implemented the utilization of it. Still HIGHLY experimental.
includes/clientside/static/ajax.js
includes/common.php
includes/plugins.php
includes/sql_parse.php
language/english/admin.json
plugins/admin/PluginManager.php
--- a/includes/clientside/static/ajax.js	Wed Apr 09 19:27:02 2008 -0400
+++ b/includes/clientside/static/ajax.js	Wed Apr 09 22:37:37 2008 -0400
@@ -1568,6 +1568,12 @@
     return true;
   }
   action = action.replace(/_confirm$/, '');
+  // white-out the plugin info box
+  if ( btnobj )
+  {
+    var td = btnobj.parentNode.parentNode.parentNode.parentNode;
+    var blackbox = whiteOutElement(td);
+  }
   var request = toJSONString({
       mode: action,
       plugin: plugin_filename
@@ -1576,52 +1582,53 @@
     {
       if ( ajax.readyState == 4 && ajax.status == 200 )
       {
-        if ( ajax.responseText == 'good' )
+        var response = String(ajax.responseText + '');
+        if ( response.substr(0, 1) != '{' )
         {
-          ajaxPage( namespace_list['Admin'] + 'PluginManager' );
+          handle_invalid_json(response);
+          return false;
         }
-        else
+        response = parseJSON(response);
+        if ( response.success )
         {
-          var response = String(ajax.responseText + '');
-          if ( response.substr(0, 1) != '{' )
+          if ( blackbox )
           {
-            handle_invalid_json(response);
-            return false;
+            blackbox.parentNode.removeChild(blackbox);
           }
-          response = parseJSON(response);
-          if ( response.mode != 'error' )
+          ajaxPage( namespace_list['Admin'] + 'PluginManager' );
+          return true;
+        } 
+        // wait for fade effect to finish its run
+        setTimeout(function()
           {
-            console.debug(response);
-            return false;
-          }
-          // wait for fade effect to finish its run
-          setTimeout(function()
-            {
-              miniPrompt(function(div)
+            miniPrompt(function(div)
+              {
+                if ( blackbox )
                 {
-                  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);
-        }
+                  blackbox.parentNode.removeChild(blackbox);
+                }
+                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/common.php	Wed Apr 09 19:27:02 2008 -0400
+++ b/includes/common.php	Wed Apr 09 22:37:37 2008 -0400
@@ -308,8 +308,8 @@
 // Load plugins from common because we can't give plugins full abilities in object context
 foreach ( $plugins->load_list as $f )
 {
-  if ( file_exists($f) )
-    include_once $f;
+  if ( file_exists(ENANO_ROOT . '/plugins/' . $f) )
+    include_once ENANO_ROOT . '/plugins/' . $f;
 }
 
 profiler_log('Loaded plugins');
--- a/includes/plugins.php	Wed Apr 09 19:27:02 2008 -0400
+++ b/includes/plugins.php	Wed Apr 09 22:37:37 2008 -0400
@@ -63,52 +63,35 @@
   
   function loadAll() 
   {
+    global $db, $session, $paths, $template, $plugins; // Common objects
     $dir = ENANO_ROOT.'/plugins/';
     
-    $this->load_list = Array();
-    
-    $plugins = Array();
+    $this->load_list = $this->system_plugins;
+    $q = $db->sql_query('SELECT plugin_filename, plugin_version FROM ' . table_prefix . 'plugins WHERE plugin_flags & ~' . PLUGIN_DISABLED . ' = plugin_flags;');
+    if ( !$q )
+      $db->_die();
     
-    // Open a known directory, and proceed to read its contents
+    while ( $row = $db->fetchrow() )
+    {
+      $this->load_list[] = $row['plugin_filename'];
+    }
+    
+    $this->loaded_plugins = $this->get_plugin_list($this->load_list);
     
-    if (is_dir($dir))
+    // check for out-of-date plugins
+    foreach ( $this->load_list as $i => $plugin )
     {
-      if ($dh = opendir($dir))
+      if ( in_array($plugin, $this->system_plugins) )
+        continue;
+      if ( $this->loaded_plugins[$plugin]['status'] & PLUGIN_OUTOFDATE )
       {
-        while (($file = readdir($dh)) !== false)
-        {
-          if(preg_match('#^(.*?)\.php$#is', $file))
-          {
-            if(getConfig('plugin_'.$file) == '1' || in_array($file, $this->system_plugins))
-            {
-              $this->load_list[] = $dir . $file;
-              $plugid = substr($file, 0, strlen($file)-4);
-              $f = @file_get_contents($dir . $file);
-              if ( empty($f) )
-                continue;
-              $f = explode("\n", $f);
-              $f = array_slice($f, 2, 7);
-              $f[0] = substr($f[0], 13);
-              $f[1] = substr($f[1], 12);
-              $f[2] = substr($f[2], 13);
-              $f[3] = substr($f[3], 8 );
-              $f[4] = substr($f[4], 9 );
-              $f[5] = substr($f[5], 12);
-              $plugins[$plugid] = Array();
-              $plugins[$plugid]['name'] = $f[0];
-              $plugins[$plugid]['uri']  = $f[1];
-              $plugins[$plugid]['desc'] = $f[2];
-              $plugins[$plugid]['auth'] = $f[3];
-              $plugins[$plugid]['vers'] = $f[4];
-              $plugins[$plugid]['aweb'] = $f[5];
-            }
-          }
-        }
-        closedir($dh);
+        // it's out of date, don't load
+        unset($this->load_list[$i]);
+        unset($this->loaded_plugins[$plugin]);
       }
     }
-    $this->loaded_plugins = $plugins;
-    //die('<pre>'.htmlspecialchars(print_r($plugins, true)).'</pre>');
+    
+    $this->load_list = array_unique($this->load_list);
   }
   
   /**
@@ -215,7 +198,6 @@
             . '#m';
             
     // Match out all blocks
-    
     $results = preg_match_all($regexp, $contents, $blocks);
     
     $return = array();
@@ -256,6 +238,549 @@
     }
     return $return;
   }
+  
+  /**
+   * Reads all plugins in the filesystem and cross-references them with the database, providing a very complete summary of plugins
+   * on the site.
+   * @param array If specified, will restrict scanned files to this list. Defaults to null, which means all PHP files will be scanned.
+   * @return array
+   */
+  
+  function get_plugin_list($restrict = null)
+  {
+    global $db, $session, $paths, $template, $plugins; // Common objects
+    
+    // Scan all plugins
+    $plugin_list = array();
+    
+    if ( $dirh = @opendir( ENANO_ROOT . '/plugins' ) )
+    {
+      while ( $dh = @readdir($dirh) )
+      {
+        if ( !preg_match('/\.php$/i', $dh) )
+          continue;
+        
+        if ( is_array($restrict) )
+          if ( !in_array($dh, $restrict) )
+            continue;
+          
+        $fullpath = ENANO_ROOT . "/plugins/$dh";
+        // it's a PHP file, attempt to read metadata
+        // pass 1: try to read a !info block
+        $blockdata = $this->parse_plugin_blocks($fullpath, 'info');
+        if ( empty($blockdata) )
+        {
+          // no !info block, check for old header
+          $fh = @fopen($fullpath, 'r');
+          if ( !$fh )
+            // can't read, bail out
+            continue;
+          $plugin_data = array();
+          for ( $i = 0; $i < 8; $i++ )
+          {
+            $plugin_data[] = @fgets($fh, 8096);
+          }
+          // close our file handle
+          fclose($fh);
+          // is the header correct?
+          if ( trim($plugin_data[0]) != '<?php' || trim($plugin_data[1]) != '/*' )
+          {
+            // nope. get out.
+            continue;
+          }
+          // parse all the variables
+          $plugin_meta = array();
+          for ( $i = 2; $i <= 7; $i++ )
+          {
+            if ( !preg_match('/^([A-z0-9 ]+?): (.+?)$/', trim($plugin_data[$i]), $match) )
+              continue 2;
+            $plugin_meta[ strtolower($match[1]) ] = $match[2];
+          }
+        }
+        else
+        {
+          // parse JSON block
+          $plugin_data =& $blockdata[0]['value'];
+          $plugin_data = enano_clean_json(enano_trim_json($plugin_data));
+          try
+          {
+            $plugin_meta_uc = enano_json_decode($plugin_data);
+          }
+          catch ( Exception $e )
+          {
+            continue;
+          }
+          // convert all the keys to lowercase
+          $plugin_meta = array();
+          foreach ( $plugin_meta_uc as $key => $value )
+          {
+            $plugin_meta[ strtolower($key) ] = $value;
+          }
+        }
+        if ( !isset($plugin_meta) || !is_array(@$plugin_meta) )
+        {
+          // parsing didn't work.
+          continue;
+        }
+        // check for required keys
+        $required_keys = array('plugin name', 'plugin uri', 'description', 'author', 'version', 'author uri');
+        foreach ( $required_keys as $key )
+        {
+          if ( !isset($plugin_meta[$key]) )
+            // not set, skip this plugin
+            continue 2;
+        }
+        // decide if it's a system plugin
+        $plugin_meta['system plugin'] = in_array($dh, $this->system_plugins);
+        // reset installed variable
+        $plugin_meta['installed'] = false;
+        $plugin_meta['status'] = 0;
+        // all checks passed
+        $plugin_list[$dh] = $plugin_meta;
+      }
+    }
+    // gather info about installed plugins
+    $q = $db->sql_query('SELECT plugin_id, 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;
+      $plugin_list[$filename]['plugin id'] = $row['plugin_id'];
+      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);
+    
+    // done
+    return $plugin_list;
+  }
+  
+  /**
+   * Installs a plugin.
+   * @param string Filename of plugin.
+   * @param array The list of plugins as output by pluginLoader::get_plugin_list(). If not passed, the function is called, possibly wasting time.
+   * @return array JSON-formatted but not encoded response
+   */
+  
+  function install_plugin($filename, $plugin_list = null)
+  {
+    global $db, $session, $paths, $template, $plugins; // Common objects
+    global $lang;
+    
+    if ( !$plugin_list )
+      $plugin_list = $this->get_plugin_list();
+    
+    // we're gonna need this
+    require_once ( ENANO_ROOT . '/includes/sql_parse.php' );
+    
+    switch ( true ): case true:
+      
+    // is the plugin in the directory and awaiting installation?
+    if ( !isset($plugin_list[$filename]) || (
+        isset($plugin_list[$filename]) && $plugin_list[$filename]['installed']
+      ))
+    {
+      $return = array(
+        'mode' => 'error',
+        'error' => 'Invalid plugin specified.',
+        'debug' => $filename
+      );
+      break;
+    }
+    
+    $dataset =& $plugin_list[$filename];
+    
+    // load up the installer schema
+    $schema = $this->parse_plugin_blocks( ENANO_ROOT . '/plugins/' . $filename, 'install' );
+    
+    $sql = array();
+    if ( !empty($schema) )
+    {
+      // parse SQL
+      $parser = new SQL_Parser($schema[0]['value'], true);
+      $parser->assign_vars(array(
+        'TABLE_PREFIX' => table_prefix
+        ));
+      $sql = $parser->parse();
+    }
+    
+    // schema is final, check queries
+    foreach ( $sql as $query )
+    {
+      if ( !$db->check_query($query) )
+      {
+        // aww crap, a query is bad
+        $return = array(
+          'mode' => 'error',
+          'error' => $lang->get('acppm_err_upgrade_bad_query'),
+        );
+        break 2;
+      }
+    }
+    
+    // this is it, perform installation
+    foreach ( $sql as $query )
+    {
+      if ( substr($query, 0, 1) == '@' )
+      {
+        $query = substr($query, 1);
+        $db->sql_query($query);
+      }
+      else
+      {
+        if ( !$db->sql_query($query) )
+          $db->die_json();
+      }
+    }
+    
+    // register plugin
+    $version_db = $db->escape($dataset['version']);
+    $filename_db = $db->escape($filename);
+    $flags = PLUGIN_INSTALLED;
+    
+    $q = $db->sql_query('INSERT INTO ' . table_prefix . "plugins ( plugin_version, plugin_filename, plugin_flags )\n"
+                      . "  VALUES ( '$version_db', '$filename_db', $flags );");
+    if ( !$q )
+      $db->die_json();
+    
+    $return = array(
+      'success' => true
+    );
+    
+    endswitch;
+    
+    return $return;
+  }
+  
+  /**
+   * Uninstalls a plugin, removing it completely from the database and calling any custom uninstallation code the plugin specifies.
+   * @param string Filename of plugin.
+   * @param array The list of plugins as output by pluginLoader::get_plugin_list(). If not passed, the function is called, possibly wasting time.
+   * @return array JSON-formatted but not encoded response
+   */
+  
+  function uninstall_plugin($filename, $plugin_list = null)
+  {
+    global $db, $session, $paths, $template, $plugins; // Common objects
+    global $lang;
+    
+    if ( !$plugin_list )
+      $plugin_list = $this->get_plugin_list();
+    
+    // we're gonna need this
+    require_once ( ENANO_ROOT . '/includes/sql_parse.php' );
+    
+    switch ( true ): case true:
+    
+    // is the plugin in the directory and already installed?
+    if ( !isset($plugin_list[$filename]) || (
+        isset($plugin_list[$filename]) && !$plugin_list[$filename]['installed']
+      ))
+    {
+      $return = array(
+        'mode' => 'error',
+        'error' => 'Invalid plugin specified.',
+      );
+      break;
+    }
+    // get plugin id
+    $dataset =& $plugin_list[$filename];
+    if ( empty($dataset['plugin id']) )
+    {
+      $return = array(
+        'mode' => 'error',
+        'error' => 'Couldn\'t retrieve plugin ID.',
+      );
+      break;
+    }
+    
+    // load up the installer schema
+    $schema = $this->parse_plugin_blocks( ENANO_ROOT . '/plugins/' . $filename, 'uninstall' );
+    
+    $sql = array();
+    if ( !empty($schema) )
+    {
+      // parse SQL
+      $parser = new SQL_Parser($schema[0]['value'], true);
+      $parser->assign_vars(array(
+        'TABLE_PREFIX' => table_prefix
+        ));
+      $sql = $parser->parse();
+    }
+    
+    // schema is final, check queries
+    foreach ( $sql as $query )
+    {
+      if ( !$db->check_query($query) )
+      {
+        // aww crap, a query is bad
+        $return = array(
+          'mode' => 'error',
+          'error' => $lang->get('acppm_err_upgrade_bad_query'),
+        );
+        break 2;
+      }
+    }
+    
+    // this is it, perform uninstallation
+    foreach ( $sql as $query )
+    {
+      if ( substr($query, 0, 1) == '@' )
+      {
+        $query = substr($query, 1);
+        $db->sql_query($query);
+      }
+      else
+      {
+        if ( !$db->sql_query($query) )
+          $db->die_json();
+      }
+    }
+    
+    // deregister plugin
+    $q = $db->sql_query('DELETE FROM ' . table_prefix . "plugins WHERE plugin_id = {$dataset['plugin id']};");
+    if ( !$q )
+      $db->die_json();
+    
+    $return = array(
+      'success' => true
+    );
+    
+    endswitch;
+    
+    return $return;
+  }
+  
+  /**
+   * Very intelligently upgrades a plugin to the version specified in the filesystem.
+   * @param string Filename of plugin.
+   * @param array The list of plugins as output by pluginLoader::get_plugin_list(). If not passed, the function is called, possibly wasting time.
+   * @return array JSON-formatted but not encoded response
+   */
+  
+  function upgrade_plugin($filename, $plugin_list = null)
+  {
+    global $db, $session, $paths, $template, $plugins; // Common objects
+    global $lang;
+    
+    if ( !$plugin_list )
+      $plugin_list = $this->get_plugin_list();
+    
+    // we're gonna need this
+    require_once ( ENANO_ROOT . '/includes/sql_parse.php' );
+    
+    switch ( true ): case true:
+    
+    // is the plugin in the directory and already installed?
+    if ( !isset($plugin_list[$filename]) || (
+        isset($plugin_list[$filename]) && !$plugin_list[$filename]['installed']
+      ))
+    {
+      $return = array(
+        'mode' => 'error',
+        'error' => 'Invalid plugin specified.',
+      );
+      break;
+    }
+    // get plugin id
+    $dataset =& $plugin_list[$filename];
+    if ( empty($dataset['plugin id']) )
+    {
+      $return = array(
+        'mode' => 'error',
+        'error' => 'Couldn\'t retrieve plugin ID.',
+      );
+      break;
+    }
+    
+    //
+    // Here we go with the main upgrade process. This is the same logic that the
+    // Enano official upgrader uses, in fact it's the same SQL parser. We need
+    // list of all versions of the plugin to continue, though.
+    //
+    
+    if ( !isset($dataset['version list']) || ( isset($dataset['version list']) && !is_array($dataset['version list']) ) )
+    {
+      // no version list - update the version number but leave the rest alone
+      $version = $db->escape($dataset['version']);
+      $q = $db->sql_query('UPDATE ' . table_prefix . "plugins SET plugin_version = '$version' WHERE plugin_id = {$dataset['plugin id']};");
+      if ( !$q )
+        $db->die_json();
+      
+      // send an error and notify the user even though it was technically a success
+      $return = array(
+        'mode' => 'error',
+        'error' => $lang->get('acppm_err_upgrade_not_supported'),
+      );
+      break;
+    }
+    
+    // build target list
+    $versions  = $dataset['version list'];
+    $indices   = array_flip($versions);
+    $installed = $dataset['version installed'];
+    
+    // is the current version upgradeable?
+    if ( !isset($indices[$installed]) )
+    {
+      $return = array(
+        'mode' => 'error',
+        'error' => $lang->get('acppm_err_upgrade_bad_version'),
+      );
+      break;
+    }
+    
+    // does the plugin support upgrading to its own version?
+    if ( !isset($indices[$installed]) )
+    {
+      $return = array(
+        'mode' => 'error',
+        'error' => $lang->get('acppm_err_upgrade_bad_target_version'),
+      );
+      break;
+    }
+    
+    // list out which versions to do
+    $index_start = @$indices[$installed] + 1;
+    $index_stop  = @$indices[$dataset['version']];
+    
+    // Are we trying to go backwards?
+    if ( $index_stop <= $index_start )
+    {
+      $return = array(
+        'mode' => 'error',
+        'error' => $lang->get('acppm_err_upgrade_to_older'),
+      );
+      break;
+    }
+    
+    // build the list of version sets
+    $ver_previous = $installed;
+    $targets = array();
+    for ( $i = $index_start; $i <= $index_stop; $i++ )
+    {
+      $targets[] = array($ver_previous, $versions[$i]);
+      $ver_previous = $versions[$i];
+    }
+    
+    // parse out upgrade sections in plugin file
+    $plugin_blocks = $this->parse_plugin_blocks( ENANO_ROOT . '/plugins/' . $filename, 'upgrade' );
+    $sql_blocks = array();
+    foreach ( $plugin_blocks as $block )
+    {
+      if ( !isset($block['from']) || !isset($block['to']) )
+      {
+        continue;
+      }
+      $key = "{$block['from']} TO {$block['to']}";
+      $sql_blocks[$key] = $block['value'];
+    }
+    
+    // do version list check
+    // for now we won't fret if a specific version set isn't found, we'll just
+    // not do that version and assume there were no DB changes.
+    foreach ( $targets as $i => $target )
+    {
+      list($from, $to) = $target;
+      $key = "$from TO $to";
+      if ( !isset($sql_blocks[$key]) )
+      {
+        unset($targets[$i]);
+      }
+    }
+    $targets = array_values($targets);
+    
+    // parse and finalize schema
+    $schema = array();
+    foreach ( $targets as $i => $target )
+    {
+      list($from, $to) = $target;
+      $key = "$from TO $to";
+      try
+      {
+        $parser = new SQL_Parser($sql_blocks[$key], true);
+      }
+      catch ( Exception $e )
+      {
+        $return = array(
+          'mode' => 'error',
+          'error' => 'SQL parser init exception',
+          'debug' => "$e"
+        );
+        break 2;
+      }
+      $parser->assign_vars(array(
+        'TABLE_PREFIX' => table_prefix
+        ));
+      $parsed = $parser->parse();
+      foreach ( $parsed as $query )
+      {
+        $schema[] = $query;
+      }
+    }
+    
+    // schema is final, check queries
+    foreach ( $schema as $query )
+    {
+      if ( !$db->check_query($query) )
+      {
+        // aww crap, a query is bad
+        $return = array(
+          'mode' => 'error',
+          'error' => $lang->get('acppm_err_upgrade_bad_query'),
+        );
+        break 2;
+      }
+    }
+    
+    // this is it, perform upgrade
+    foreach ( $schema as $query )
+    {
+      if ( substr($query, 0, 1) == '@' )
+      {
+        $query = substr($query, 1);
+        $db->sql_query($query);
+      }
+      else
+      {
+        if ( !$db->sql_query($query) )
+          $db->die_json();
+      }
+    }
+    
+    // update version number
+    $version = $db->escape($dataset['version']);
+    $q = $db->sql_query('UPDATE ' . table_prefix . "plugins SET plugin_version = '$version' WHERE plugin_id = {$dataset['plugin id']};");
+    if ( !$q )
+      $db->die_json();
+    
+    // all done :-)
+    $return = array(
+      'success' => true
+    );
+    
+    endswitch;
+    
+    return $return;
+  }
 }
 
 ?>
--- a/includes/sql_parse.php	Wed Apr 09 19:27:02 2008 -0400
+++ b/includes/sql_parse.php	Wed Apr 09 22:37:37 2008 -0400
@@ -50,11 +50,12 @@
   /**
    * Constructor.
    * @param string If this contains newlines, it will be treated as the target SQL. If not, will be treated as a filename.
+   * @param string If true, force as raw SQL, i.e. don't treat as a filename no matter what
    */
   
-  public function __construct($sql)
+  public function __construct($sql, $force_file = false)
   {
-    if ( strpos($sql, "\n") )
+    if ( strpos($sql, "\n") || $force_file )
     {
       $this->sql_string = $sql;
     }
--- a/language/english/admin.json	Wed Apr 09 19:27:02 2008 -0400
+++ b/language/english/admin.json	Wed Apr 09 22:37:37 2008 -0400
@@ -400,8 +400,18 @@
       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_uninstall: 'Uninstalling this plugin may cause the loss of data that was created with it. You should only uninstall a plugin if you are certain you\'ll have no further use for it; in fact, you don\'t even need to uninstall a plugin if you\'re deleting it from the filesystem.',
       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.',
+      
+      err_upgrade_not_supported: 'This plugin doesn\'t support automatic upgrades. The version number has been updated so the plugin will be re-enabled, but you should check the plugin file to see if the author provided instructions for finishing the upgrade.',
+      err_upgrade_bad_version: 'This plugin cannot be upgraded because you are running a version of the plugin that is not listed in the plugin\'s version list.',
+      err_upgrade_bad_target_version: 'This plugin cannot be upgraded because it does not support its own version. Please contact the author and ask them to fix this.',
+      err_upgrade_to_older: 'You are trying to upgrade to an older release of this plugin. This is unsupported and must be done manually.',
+      err_upgrade_bad_query: 'There is a problem with one of the SQL queries the plugin is trying to make.',
+      
+      msg_old_entries_title: 'Import old plugin installation data',
+      msg_old_entries_body: 'There is still data from the old plugin structure in your database. You can import this to the new structure automatically using the button below.',
+      btn_import_old: 'Import old plugin settings',
     },
     acppm: {
       heading_main: 'Edit page properties',
--- a/plugins/admin/PluginManager.php	Wed Apr 09 19:27:02 2008 -0400
+++ b/plugins/admin/PluginManager.php	Wed Apr 09 22:37:37 2008 -0400
@@ -26,7 +26,7 @@
  * 
  * The format for the special comment blocks is:
  <code>
- /**!blocktype( param1 = "value1" [ param2 = "value2" ... ] )**
+ /**!blocktype( param1 = "value1"; [ param2 = "value2"; ... ] )**
  
  ... block content ...
  
@@ -58,7 +58,9 @@
  <code>
  /**!language**
  {
+   // each entry at this level should be an ISO-639-1 language code.
    eng: {
+     // from here on in is the standard langauge file format
      categories: [ 'meta', 'foo', 'bar' ],
      strings: {
        meta: {
@@ -86,13 +88,17 @@
  </code>
  * And finally, the format for upgrade schemas:
  <code>
- /**!upgrade from = "0.1-alpha1" to = "0.1-alpha2" **
+ /**!upgrade from = "0.1-alpha1"; to = "0.1-alpha2"; **
  
  **!* /
  </code>
+ * As a courtesy to your users, we ask that you also include an "uninstall" block that reverses any changes your plugin makes
+ * to the database upon installation. The syntax is identical to that of the install block.
+ * 
  * Remember that upgrades will always be done incrementally, so if the user is upgrading 0.1-alpha2 to 0.1, Enano's plugin
  * engine will run the 0.1-alpha2 to 0.1-beta1 upgrader, then the 0.1-beta1 to 0.1 upgrader, going by the versions listed in
- * the example metadata block above.
+ * the example metadata block above. As with the standard Enano installer, prefixing a query with '@' will cause it to be
+ * performed "blindly", e.g. not checked for errors.
  * 
  * All of this information is effective as of Enano 1.1.4.
  */
@@ -111,6 +117,8 @@
     return;
   }
   
+  $plugin_list = $plugins->get_plugin_list();
+  
   // Are we processing an AJAX request from the smartform?
   if ( $paths->getParam(0) == 'action.json' )
   {
@@ -133,6 +141,120 @@
         {
           switch ( $request['mode'] )
           {
+            case 'install':
+              // did they specify a plugin to operate on?
+              if ( !isset($request['plugin']) )
+              {
+                $return = array(
+                  'mode' => 'error',
+                  'error' => 'No plugin specified.',
+                );
+                break;
+              }
+              
+              $return = $plugins->install_plugin($request['plugin'], $plugin_list);
+              break;
+            case 'upgrade':
+              // did they specify a plugin to operate on?
+              if ( !isset($request['plugin']) )
+              {
+                $return = array(
+                  'mode' => 'error',
+                  'error' => 'No plugin specified.',
+                );
+                break;
+              }
+              
+              $return = $plugins->upgrade_plugin($request['plugin'], $plugin_list);
+              break;
+            case 'uninstall':
+              // did they specify a plugin to operate on?
+              if ( !isset($request['plugin']) )
+              {
+                $return = array(
+                  'mode' => 'error',
+                  'error' => 'No plugin specified.',
+                );
+                break;
+              }
+              
+              $return = $plugins->uninstall_plugin($request['plugin'], $plugin_list);
+              break;
+            case 'disable':
+            case 'enable':
+              $flags_col = ( $request['mode'] == 'disable' ) ?
+                            "plugin_flags | "  . PLUGIN_DISABLED :
+                            "plugin_flags & ~" . PLUGIN_DISABLED;
+              // did they specify a plugin to operate on?
+              if ( !isset($request['plugin']) )
+              {
+                $return = array(
+                  'mode' => 'error',
+                  'error' => 'No plugin specified.',
+                );
+                break;
+              }
+              // is the plugin in the directory and already installed?
+              if ( !isset($plugin_list[$request['plugin']]) || (
+                  isset($plugin_list[$request['plugin']]) && !$plugin_list[$request['plugin']]['installed']
+                ))
+              {
+                $return = array(
+                  'mode' => 'error',
+                  'error' => 'Invalid plugin specified.',
+                );
+                break;
+              }
+              // get plugin id
+              $dataset =& $plugin_list[$request['plugin']];
+              if ( empty($dataset['plugin id']) )
+              {
+                $return = array(
+                  'mode' => 'error',
+                  'error' => 'Couldn\'t retrieve plugin ID.',
+                );
+                break;
+              }
+              // perform update
+              $q = $db->sql_query('UPDATE ' . table_prefix . "plugins SET plugin_flags = $flags_col WHERE plugin_id = {$dataset['plugin id']};");
+              if ( !$q )
+                $db->die_json();
+              
+              $return = array(
+                'success' => true
+              );
+              break;
+            case 'import':
+              // import all of the plugin_* config entries
+              $q = $db->sql_query('SELECT config_name, config_value FROM ' . table_prefix . "config WHERE config_name LIKE 'plugin_%';");
+              if ( !$q )
+                $db->die_json();
+              
+              while ( $row = $db->fetchrow($q) )
+              {
+                $plugin_filename = preg_replace('/^plugin_/', '', $row['config_name']);
+                if ( isset($plugin_list[$plugin_filename]) && !@$plugin_list[$plugin_filename]['installed'] )
+                {
+                  $return = $plugins->install_plugin($plugin_filename, $plugin_list);
+                  if ( !$return['success'] )
+                    break 2;
+                  if ( $row['config_value'] == '0' )
+                  {
+                    $fn_db = $db->escape($plugin_filename);
+                    $q = $db->sql_query('UPDATE ' . table_prefix . "plugins SET plugin_flags = plugin_flags | " . PLUGIN_DISABLED . " WHERE plugin_filename = '$fn_db';");
+                    if ( !$q )
+                      $db->die_json();
+                  }
+                }
+              }
+              $db->free_result($q);
+              
+              $q = $db->sql_query('DELETE FROM ' . table_prefix . "config WHERE config_name LIKE 'plugin_%';");
+              if ( !$q )
+                $db->die_json();
+              
+              $return = array('success' => true);
+              break;
             default:
               // The requested action isn't something this script knows how to do
               $return = array(
@@ -178,121 +300,7 @@
   // Not a JSON request, output normal HTML interface
   //
   
-  // Scan all plugins
-  $plugin_list = array();
-  
-  if ( $dirh = @opendir( ENANO_ROOT . '/plugins' ) )
-  {
-    while ( $dh = @readdir($dirh) )
-    {
-      if ( !preg_match('/\.php$/i', $dh) )
-        continue;
-      $fullpath = ENANO_ROOT . "/plugins/$dh";
-      // it's a PHP file, attempt to read metadata
-      // pass 1: try to read a !info block
-      $blockdata = $plugins->parse_plugin_blocks($fullpath, 'info');
-      if ( empty($blockdata) )
-      {
-        // no !info block, check for old header
-        $fh = @fopen($fullpath, 'r');
-        if ( !$fh )
-          // can't read, bail out
-          continue;
-        $plugin_data = array();
-        for ( $i = 0; $i < 8; $i++ )
-        {
-          $plugin_data[] = @fgets($fh, 8096);
-        }
-        // close our file handle
-        fclose($fh);
-        // is the header correct?
-        if ( trim($plugin_data[0]) != '<?php' || trim($plugin_data[1]) != '/*' )
-        {
-          // nope. get out.
-          continue;
-        }
-        // parse all the variables
-        $plugin_meta = array();
-        for ( $i = 2; $i <= 7; $i++ )
-        {
-          if ( !preg_match('/^([A-z0-9 ]+?): (.+?)$/', trim($plugin_data[$i]), $match) )
-            continue 2;
-          $plugin_meta[ strtolower($match[1]) ] = $match[2];
-        }
-      }
-      else
-      {
-        // parse JSON block
-        $plugin_data =& $blockdata[0]['value'];
-        $plugin_data = enano_clean_json(enano_trim_json($plugin_data));
-        try
-        {
-          $plugin_meta_uc = enano_json_decode($plugin_data);
-        }
-        catch ( Exception $e )
-        {
-          continue;
-        }
-        // convert all the keys to lowercase
-        $plugin_meta = array();
-        foreach ( $plugin_meta_uc as $key => $value )
-        {
-          $plugin_meta[ strtolower($key) ] = $value;
-        }
-      }
-      if ( !isset($plugin_meta) || !is_array(@$plugin_meta) )
-      {
-        // parsing didn't work.
-        continue;
-      }
-      // check for required keys
-      $required_keys = array('plugin name', 'plugin uri', 'description', 'author', 'version', 'author uri');
-      foreach ( $required_keys as $key )
-      {
-        if ( !isset($plugin_meta[$key]) )
-          // not set, skip this plugin
-          continue 2;
-      }
-      // 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;
-    }
-  }
-  // 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">
@@ -324,7 +332,7 @@
         {
           $color = '_red';
           $status = $lang->get('acppl_lbl_status_need_upgrade');
-          $buttons = 'uninstall|update';
+          $buttons = 'uninstall|upgrade';
         }
         else if ( $data['installed'] && $data['status'] & PLUGIN_DISABLED )
         {
@@ -389,7 +397,7 @@
                   <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();\"
+                  <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>
@@ -410,5 +418,18 @@
     </table>
   </div>
   <?php
-  echo '</form>';
+  // are there still old style plugin entries?
+  $q = $db->sql_query('SELECT 1 FROM ' . table_prefix . "config WHERE config_name LIKE 'plugin_%';");
+  if ( !$q )
+    $db->_die();
+  
+  $count = $db->numrows();
+  $db->free_result($q);
+  
+  if ( $count > 0 )
+  {
+    echo '<h3>' . $lang->get('acppl_msg_old_entries_title') . '</h3>';
+    echo '<p>' . $lang->get('acppl_msg_old_entries_body') . '</p>';
+    echo '<p><a class="abutton abutton_green" href="#" onclick="ajaxPluginAction(\'import\', \'\', false); return false;">' . $lang->get('acppl_btn_import_old') . '</a></p>';
+  }
 }