includes/plugins.php
author Dan Fuhry <dan@enanocms.org>
Wed, 29 Dec 2010 13:25:32 -0500
changeset 1332 12286b3ee214
parent 1227 bdac73ed481e
child 1368 b19698a37d58
permissions -rw-r--r--
Added some more hooks to the page editing pipeline. It should now be possible to add controls to the page editor, send the data from them out to the server, and process them on the server side.

<?php

/*
 * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
 * Copyright (C) 2006-2009 Dan Fuhry
 *
 * 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.
 */
 
/**
 * Class used to handle and process plugin requests and loading. Singleton.
 * @package Enano
 * @author Dan Fuhry <dan@enanocms.org>
 * @copyright (C) 2006-2008 Enano Project
 * @license GNU General Public License <http://enanocms.org/Special:GNU_General_Public_License>
 */

class pluginLoader {
	
	/**
 	* The list of hooks registered.
 	* @var array
 	* @access private
 	*/
	
	var $hook_list;
	
	/**
 	* The list of plugins that should be loaded. Used only by common.php.
 	* @var array
 	* @access private
 	*/
	
	var $load_list;
	
	/**
 	* The list of plugins that are loaded currently. This is only used by the loaded() method which in turn is
 	* used by template files with the <!-- IFPLUGIN --> special tag.
 	* @var array
 	* @access private
 	*/
	
	var $loaded_plugins;
	
	/**
 	* The list of plugins that are always loaded because they're part of the Enano core. This cannot be modified
 	* by any external code because user plugins are loaded after the load_list is calculated. Can be useful in
 	* alternative administration panel frameworks that need the list of system plugins.
 	* @var array
 	*/
	
	var $system_plugins = Array('SpecialUserFuncs.php','SpecialUserPrefs.php','SpecialPageFuncs.php','SpecialAdmin.php','SpecialCSS.php','SpecialUpdownload.php','SpecialSearch.php','PrivateMessages.php','SpecialGroups.php', 'SpecialLog.php', 'DemoMode.php');
	
	/**
 	* Name kept for compatibility. Effectively a constructor. Calculates the list of plugins that should be loaded
 	* and puts that list in the $load_list property. Plugin developers have absolutely no use for this whatsoever.
 	*/
	
	function loadAll() 
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		$GLOBALS['plugins_cache'] = array();
		
		// if we're in an upgrade, just skip this step
		if ( defined('IN_ENANO_UPGRADE') )
		{
			$this->load_list = array();
			return false;
		}
		
		$dir = ENANO_ROOT.'/plugins/';
		
		$plugin_list = $this->get_plugin_list();
		$this->load_list = array();
	
		foreach ( $plugin_list as $filename => $data )
		{
			if ( !$data['system plugin'] && ( $data['status'] & PLUGIN_OUTOFDATE || $data['status'] & PLUGIN_DISABLED || !$data['installed'] ) )
				continue;
			
			$this->load_list[] = $filename;
			$this->loaded_plugins[$filename] = $data;
		}
	}
	
	/**
 	* Name kept for compatibility. This method is used to add a new hook into the code somewhere. Plugins are encouraged
 	* to set hooks and hook into other plugins in a fail-safe way, this encourages reuse of code. Returns an array, whose
 	* values should be eval'ed.
 	* @example <code>
 	$code = $plugins->setHook('my_hook_name');
 	foreach ( $code as $cmd )
 	{
 		eval($cmd);
 	}
 	</code>
 	* @param string The name of the hook.
 	* @param array Deprecated.
 	*/
	
	function setHook($name, $dont_split = false)
	{
		if ( !empty($this->hook_list[$name]) && is_array($this->hook_list[$name]) )
		{
			if ( $dont_split )
				return $this->hook_list[$name];
			
			return array(implode("\n", $this->hook_list[$name]));
		}
		else
		{
			return Array();
		}
	}
	
	/**
 	* Attaches to a hook effectively scheduling some code to be run at that point. You should try to keep hooks clean by
 	* making a function that has variables that need to be modified passed by reference.
 	* @example Simple example: <code>
 	$plugins->attachHook('render_wikiformat_pre', '$text = str_replace("Goodbye, Mr. Chips", "Hello, Mr. Carrots", $text);');
 	</code>
 	* @example More complicated example: <code>
 	$plugins->attachHook('render_wikiformat_pre', 'myplugin_parser_ext($text);');
 	
 	// Notice that $text is passed by reference.
 	function myplugin_parser_ext(&$text)
 	{
 		$text = str_replace("Goodbye, Mr. Chips", "Hello, Mr. Carrots", $text);
 	}
 	</code>
 	*/
	
	function attachHook($name, $code)
	{
		if ( !isset($this->hook_list[$name]) )
		{
			$this->hook_list[$name] = Array();
		}
		$this->hook_list[$name][] = $code;
	}
	
	/**
 	* Tell whether a plugin is loaded or not.
 	* @param string The filename of the plugin
 	* @return bool
 	*/
	
	function loaded($plugid)
	{
		return isset( $this->loaded_plugins[$plugid] );
	}
	
	/**
 	* Parses all special comment blocks in a plugin and returns an array in the format:
 	<code>
 	array(
 			0 => array(
 					'block' => 'upgrade',
 					// parsed from the block's parameters section
 						'release_from' => '1.0b1',
 						'release_to' => '1.0b2',
 					'value' => 'foo'
 				),
 			1 => array(
 					...
 				)
 		);
 	</code>
 	* @param string Path to plugin file
 	* @param string Optional. The type of block to fetch. If this is specified, only the block type specified will be read, all others will be discarded.
 	* @return array
 	*/
	
	public static function parse_plugin_blocks($file, $type = false)
	{
		if ( !file_exists($file) )
		{
			return array();
		}
		$blocks = array();
		$contents = @file_get_contents($file);
		if ( empty($contents) )
		{
			return array();
		}
		
		// The "\r?" in this regular expression is the result of an embarrassing failure during a job related meeting.
		// I was demoing the authoring of an Enano plugin and simply could not figure out why the plugin would not
		// show up in the admin panel.

		$regexp = '#^/\*\*!([a-z0-9_]+)'  // block header and type
						. '(([\s]+[a-z0-9_]+[\s]*=[\s]*".+?"[\s]*;)*)' // parameters
						. '[\s]*\*\*' . "\r?\n"      // spacing and header close
						. '([\w\W]+?)' . "\r?\n"     // value
						. '\*\*!\*/'              // closing comment
						. '#m';
						
		// Match out all blocks
		$results = preg_match_all($regexp, $contents, $blocks);
		
		$return = array();
		foreach ( $blocks[0] as $i => $_ )
		{
			if ( is_string($type) && $blocks[1][$i] !== $type )
				continue;
			
			$value =& $blocks[4][$i];
			// parse includes
			preg_match_all('/^!include [\'"]?(.+?)[\'"]?$/m', $value, $includes);
			foreach ( $includes[0] as $i => $replace )
			{
				$filename = ENANO_ROOT . '/' . $includes[1][$i];
				if ( @file_exists( $filename ) && @is_readable( $filename ) )
				{
					$contents = @file_get_contents($filename);
					$value = str_replace_once($replace, $contents, $value);
				}
			}
			
			$el = self::parse_vars($blocks[2][$i]);
			$el['block'] = $blocks[1][$i];
			$el['value'] = $value;
			$return[] = $el;
		}
		
		return $return;
	}
	
	private static function parse_vars($var_block)
	{
		preg_match_all('/[\s]+([a-z0-9_]+)[\s]*=[\s]*"(.+?)";/', $var_block, $matches);
		$return = array();
		foreach ( $matches[0] as $i => $_ )
		{
			$return[ $matches[1][$i] ] = $matches[2][$i];
		}
		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.
 	* @param bool If true, allows using cached information. Defaults to true.
 	* @return array
 	*/
	
	function get_plugin_list($restrict = null, $use_cache = true)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		
		// Scan all plugins
		$plugin_list = array();
		$ta = 0;
		// won't load twice (failsafe automatic skip)
		$this->load_plugins_cache();
		global $plugins_cache;
		if ( $use_cache && !empty($plugins_cache) )
		{
			return $plugins_cache;
		}
		else
		{
			// blank array - effectively skips importing the cache
			$plugins_cache = array();
		}
		
		// List all plugins
		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;
					
				// it's a PHP file, attempt to read metadata
				$fullpath = ENANO_ROOT . "/plugins/$dh";
				$plugin_meta = $this->read_plugin_headers($fullpath, $use_cache);
				
				if ( is_array($plugin_meta) )
				{
					// all checks passed
					$plugin_list[$dh] = $plugin_meta;
				}
			}
		}
		
		// Populate with additional metadata from database
		$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;
	}
	
	/**
 	* Retrieves the metadata block from a plugin file
 	* @param string Path to plugin file (full path)
 	* @return array
 	*/
	
	function read_plugin_headers($fullpath, $use_cache = true)
	{
		global $plugins_cache;
		$dh = basename($fullpath);
		
		// first can we use cached info?
		if ( isset($plugins_cache[$dh]) && $plugins_cache[$dh]['file md5'] === $this->md5_header($fullpath) )
		{
			$plugin_meta = $plugins_cache[$dh];
		}
		else
		{
			// the cache is out of date if we reached here -- regenerate
			if ( $use_cache )
				$this->generate_plugins_cache();
			
			// 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
					return false;
				$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.
					return false;
				}
				// parse all the variables
				$plugin_meta = array();
				for ( $i = 2; $i <= 7; $i++ )
				{
					if ( !preg_match('/^([A-z0-9 ]+?): (.+?)$/', trim($plugin_data[$i]), $match) )
						return false;
					$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 )
				{
					return false;
				}
				// 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.
			return false;
		}
		// 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
				return false;
		}
		// 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;
		
		return $plugin_meta;
	}
	
	
	/**
 	* Attempts to cache plugin information in a file to speed fetching.
 	*/
	
	function generate_plugins_cache()
	{
		if ( getConfig('cache_thumbs') != '1' )
			return;
		
		// fetch the most current info
		$plugin_info = $this->get_plugin_list(null, false);
		foreach ( $plugin_info as $plugin => &$info )
		{
			$info['file md5'] = $this->md5_header(ENANO_ROOT . "/plugins/$plugin");
		}
		
		$this->update_plugins_cache($plugin_info);
		$GLOBALS['plugins_cache'] = $plugin_info;
		
		return true;
	}
	
	/**
 	* Writes an information array to the cache file.
 	* @param array
 	* @access private
 	*/
	
	function update_plugins_cache($plugin_info)
	{
		global $cache;
		try
		{
			$result = $cache->store('plugins', $plugin_info, -1);
		}
		catch ( Exception $e )
		{
			return false;
		}
		return $result;
	}
	
	/**
 	* Loads the plugins cache if any.
 	*/
	
	function load_plugins_cache()
	{
		global $cache;
		if ( $data = $cache->fetch('plugins') )
		{
			$GLOBALS['plugins_cache'] = $data;
		}
	}
	
	/**
 	* Calculates the MD5 sum of the first 10 lines of a file. Useful for caching plugin header information.
 	* @param string File
 	* @return string
 	*/
	
	function md5_header($file)
	{
		$fh = @fopen($file, 'r');
		if ( !$fh )
			return false;
		$i = 0;
		$h = '';
		while ( $i < 10 )
		{
			$line = fgets($fh, 8096);
			$h .= $line . "\n";
			$i++;
		}
		fclose($fh);
		return md5($h);
	}
	
	/**
 	* Determines if a file is an authentication extension by looking at the file contents.
 	* @param string Plugin filename
 	* @return bool
 	*/
	
	function is_file_auth_plugin($filename)
	{
		$filename = ENANO_ROOT . '/plugins/' . $filename;
		if ( !file_exists($filename) )
			return false;
		
		$info = $this->read_plugin_headers($filename);
		if ( isset($info['auth plugin']) )
			return true;
		
		$contents = @file_get_contents($filename);
		if ( strstr($contents, 'login_process_userdata_json') )
			return true;
		
		return false;
	}
	
	/**
 	* 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;
		global $cache;
		
		if ( defined('ENANO_DEMO_MODE') )
		{
			return array(
					'mode' => 'error',
					'error' => $lang->get('acppl_err_demo_mode')
				);
		}
		
		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();
		global $dbdriver;
		if ( !empty($schema) )
		{
			// Decide which schema to use
			$use_schema = false;
			foreach ( $schema as $current_schema )
			{
				if ( isset($current_schema['dbms']) && $current_schema['dbms'] === $dbdriver )
				{
					$use_schema =& $current_schema['value'];
					break;
				}
			}
			if ( !$use_schema )
			{
				if ( !isset($schema[0]['dbms']) )
				{
					$use_schema =& $schema[0]['value'];
				}
				else
				{
					$return = array(
						'mode' => 'error',
						'error' => $lang->get('acppl_err_dmbs_not_supported', array('dbdriver' => $db->dbms_name))
					);
					break;
				}
			}
			// parse SQL
			$parser = new SQL_Parser($use_schema, 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('acppl_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) )
				{
					$return = array(
							'mode' => 'error',
							'error' => "[SQL] " . $db->sql_error()
						);
					break 2;
				}
			}
		}
		
		// log action
		$time        = time();
		$ip_db       = $db->escape($_SERVER['REMOTE_ADDR']);
		$username_db = $db->escape($session->username);
		$file_db     = $db->escape($filename);
		$q = $db->sql_query('INSERT INTO '.table_prefix."logs(log_type, action, time_id, edit_summary, author, author_uid, page_text) VALUES\n"
											. "  ('security', 'plugin_install', $time, '$ip_db', '$username_db', $session->user_id, '$file_db');");
		if ( !$q )
			$db->_die();
		
		// 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();
		
		$plugin_list[$filename]['installed'] = true;
		$this->reimport_plugin_strings($filename, $plugin_list);
		
		$return = array(
			'success' => true
		);
		
		endswitch;
		
		$cache->purge('plugins');
		$cache->purge('page_meta');
		$cache->purge('anon_sidebar');
		
		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;
		global $cache;
		
		if ( defined('ENANO_DEMO_MODE') )
		{
			return array(
					'mode' => 'error',
					'error' => $lang->get('acppl_err_demo_mode')
				);
		}
		
		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();
		global $dbdriver;
		if ( !empty($schema) )
		{
			// Decide which schema to use
			$use_schema = false;
			foreach ( $schema as $current_schema )
			{
				if ( isset($current_schema['dbms']) && $current_schema['dbms'] === $dbdriver )
				{
					$use_schema =& $current_schema['value'];
					break;
				}
			}
			if ( !$use_schema )
			{
				if ( !isset($schema[0]['dbms']) )
				{
					$use_schema =& $schema[0]['value'];
				}
				else
				{
					$return = array(
						'mode' => 'error',
						'error' => $lang->get('acppl_err_dmbs_not_supported', array('dbdriver' => $db->dbms_name))
					);
					break;
				}
			}
			// parse SQL
			$parser = new SQL_Parser($use_schema, 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('acppl_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) )
				{
					$return = array(
							'mode' => 'error',
							'error' => "[SQL] " . $db->sql_error()
						);
					break 2;
				}
			}
		}
		
		// log action
		$time        = time();
		$ip_db       = $db->escape($_SERVER['REMOTE_ADDR']);
		$username_db = $db->escape($session->username);
		$file_db     = $db->escape($filename);
		$q = $db->sql_query('INSERT INTO '.table_prefix."logs(log_type, action, time_id, edit_summary, author, author_uid, page_text) VALUES\n"
											. "  ('security', 'plugin_uninstall', $time, '$ip_db', '$username_db', $session->user_id, '$file_db');");
		if ( !$q )
			$db->_die();
		
		// 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;
		
		$cache->purge('plugins');
		$cache->purge('page_meta');
		$cache->purge('anon_sidebar');
		
		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, $cache;
		
		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('acppl_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('acppl_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('acppl_err_upgrade_bad_target_version'),
			);
			break;
		}
		
		// list out which versions to do
		$index_start = @$indices[$installed];
		$index_stop  = @$indices[$dataset['version']];
		
		// Are we trying to go backwards?
		if ( $index_stop <= $index_start )
		{
			$return = array(
				'mode' => 'error',
				'error' => $lang->get('acppl_err_upgrade_to_older'),
				// 'debug' => "going from $installed ($index_start) to {$dataset['version']} ($index_stop)"
			);
			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('acppl_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) )
				{
					$return = array(
							'mode' => 'error',
							'error' => "[SQL] " . $db->sql_error()
						);
					break 2;
				}
			}
		}
		
		// log action
		$time        = time();
		$ip_db       = $db->escape($_SERVER['REMOTE_ADDR']);
		$username_db = $db->escape($session->username);
		$file_db     = $db->escape($filename);
		$q = $db->sql_query('INSERT INTO '.table_prefix."logs(log_type, action, time_id, edit_summary, author, author_uid, page_text) VALUES\n"
											. "  ('security', 'plugin_upgrade', $time, '$ip_db', '$username_db', $session->user_id, '$file_db');");
		if ( !$q )
			$db->_die();
		
		// 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;
		
		$cache->purge('plugins');
		$cache->purge('page_meta');
		$cache->purge('anon_sidebar');
		
		return $return;
	}
	
	/**
 	* Re-imports the language strings from a plugin.
 	* @param string File name
 	* @return array Enano JSON response protocol
 	*/
	
	function reimport_plugin_strings($filename, $plugin_list = null)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $lang;
		
		if ( !$plugin_list )
			$plugin_list = $this->get_plugin_list();
		
		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 data
		$dataset =& $plugin_list[$filename];
		
		// check for a language block
		$blocks = self::parse_plugin_blocks(ENANO_ROOT . '/plugins/' . $filename, 'language');
		if ( count($blocks) < 1 )
		{
			return array(
					'mode' => 'error',
					'error' => $lang->get('acppl_err_import_no_strings')
				);
		}
		
		$result = $lang->import_plugin(ENANO_ROOT . '/plugins/' . $filename);
		if ( $result )
		{
			return array(
				'success' => true
			);
		}
		else
		{
			return array(
				'mode' => 'error',
				'error' => 'Language API returned error'
			);
		}
		
		endswitch;
	}
}

?>