Revamped some ACL code and added effective permissions calculation code into session manager
<?php
/*
* Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
* Version 1.1.4 (Caoineag alpha 4)
* Copyright (C) 2006-2008 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', 'SpecialRecentChanges.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
$dir = ENANO_ROOT.'/plugins/';
$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();
while ( $row = $db->fetchrow() )
{
$this->load_list[] = $row['plugin_filename'];
}
$this->loaded_plugins = $this->get_plugin_list($this->load_list);
// check for out-of-date plugins
foreach ( $this->load_list as $i => $plugin )
{
if ( in_array($plugin, $this->system_plugins) )
continue;
if ( $this->loaded_plugins[$plugin]['status'] & PLUGIN_OUTOFDATE )
{
// it's out of date, don't load
unset($this->load_list[$i]);
unset($this->loaded_plugins[$plugin]);
}
}
$this->load_list = array_unique($this->load_list);
}
/**
* 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, $opts = Array()) {
if(isset($this->hook_list[$name]) && is_array($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();
}
$regexp = '#^/\*\*!([a-z0-9_]+)' // block header and type
. '(([\s]+[a-z0-9_]+[\s]*=[\s]*".+?"[\s]*;)*)' // parameters
. '[\s]*\*\*' . "\n" // spacing and header close
. '([\w\W]+?)' . "\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.
* @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('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) )
$db->die_json();
}
}
// 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, page_text) VALUES\n"
. " ('security', 'plugin_install', $time, '$ip_db', '$username_db', '$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();
$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('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) )
$db->die_json();
}
}
// 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, page_text) VALUES\n"
. " ('security', 'plugin_uninstall', $time, '$ip_db', '$username_db', '$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;
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('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] + 1;
$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'),
);
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) )
$db->die_json();
}
}
// 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, page_text) VALUES\n"
. " ('security', 'plugin_upgrade', $time, '$ip_db', '$username_db', '$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;
return $return;
}
}
?>