Added CLI installer. Supports interactive, command-line, and internal-call installation. Fixed a few bugs related to anti-SQL injection parser and plugin installation.
authorDan
Wed, 14 Jan 2009 20:33:05 -0500
changeset 812 68060328e9c6
parent 811 5c807fe77020
child 813 3fe11491f512
Added CLI installer. Supports interactive, command-line, and internal-call installation. Fixed a few bugs related to anti-SQL injection parser and plugin installation.
includes/common.php
includes/common_cli.php
includes/dbal.php
includes/lang.php
includes/plugins.php
install/includes/cli-core.php
install/includes/common.php
install/includes/libenanoinstallcli.php
install/install-cli.php
language/english/admin.json
language/english/install.json
language/english/user.json
plugins/SpecialAdmin.php
plugins/SpecialPageFuncs.php
plugins/admin/PluginManager.php
--- a/includes/common.php	Sun Jan 11 21:37:49 2009 -0500
+++ b/includes/common.php	Wed Jan 14 20:33:05 2009 -0500
@@ -178,9 +178,9 @@
 $dst_params = array(0, 0, 0, 0, 60);
 
 // Divert to CLI loader if running from CLI
-if ( isset($argc) && isset($argv) )
+if ( defined('ENANO_CLI') || ( isset($argc) && isset($argv) ) )
 {
-  if ( is_int($argc) && is_array($argv) && !isset($_SERVER['REQUEST_URI']) )
+  if ( defined('ENANO_CLI') || ( is_int($argc) && is_array($argv) && !isset($_SERVER['REQUEST_URI']) ) )
   {
     require(ENANO_ROOT . '/includes/common_cli.php');
     return;
--- a/includes/common_cli.php	Sun Jan 11 21:37:49 2009 -0500
+++ b/includes/common_cli.php	Wed Jan 14 20:33:05 2009 -0500
@@ -25,7 +25,7 @@
 //
 
 // Note to important functions and the template class that we're running via CLI
-define('ENANO_CLI', 1);
+@define('ENANO_CLI', 1);
 
 // The first thing we need to do is start the database connection. At this point, for all we know, Enano might not
 // even be installed. If this connection attempt fails and it's because of a missing or corrupt config file, the
--- a/includes/dbal.php	Sun Jan 11 21:37:49 2009 -0500
+++ b/includes/dbal.php	Wed Jan 14 20:33:05 2009 -0500
@@ -315,14 +315,17 @@
     $ts = microtime_float();
     
     // remove properly escaped quotes
+    $q = str_replace('\\\\', '', $q);
     $q = str_replace(array("\\\"", "\\'"), '', $q);
     
     // make sure quotes match
     foreach ( array("'", '"') as $quote )
     {
-      if ( get_char_count($q, $quote) % 2 == 1 )
+      $n_quotes = get_char_count($q, $quote);
+      if ( $n_quotes % 2 == 1 )
       {
         // mismatched quotes
+        if ( $debug ) echo "Found mismatched quotes in query; parsed:\n$q\n";
         return false;
       }
       // this quote is now confirmed to be matching; we can safely move all quoted strings out and replace with a token
--- a/includes/lang.php	Sun Jan 11 21:37:49 2009 -0500
+++ b/includes/lang.php	Wed Jan 14 20:33:05 2009 -0500
@@ -452,7 +452,7 @@
     
     $first_lang = $supported_langs[0];
     
-    return $this->import_array($langdata[$first_lang]);
+    return $this->import_array($langdata[$first_lang], false, true);
   }
   
   /**
--- a/includes/plugins.php	Sun Jan 11 21:37:49 2009 -0500
+++ b/includes/plugins.php	Wed Jan 14 20:33:05 2009 -0500
@@ -648,6 +648,8 @@
     
     endswitch;
     
+    $this->reimport_plugin_strings($filename, $plugin_list);
+    
     $cache->purge('plugins');
     $cache->purge('page_meta');
     $cache->purge('anon_sidebar');
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/install/includes/cli-core.php	Wed Jan 14 20:33:05 2009 -0500
@@ -0,0 +1,633 @@
+<?php
+
+/*
+ * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
+ * Version 1.1.6 (Caoineag beta 1)
+ * Copyright (C) 2006-2008 Dan Fuhry
+ * Installation package
+ * cli-core.php - CLI installation wizard/core
+ *
+ * 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.
+ * 
+ * Thanks to Stephan for helping out with l10n in the installer (his work is in includes/stages/*.php).
+ */
+
+require(dirname(__FILE__) . '/common.php');
+if ( !defined('ENANO_CLI') )
+{
+  $ui = new Enano_Installer_UI('Enano installation', false);
+  $ui->set_visible_stage($ui->add_stage('Error', true));
+
+  $ui->step = 'Access denied';  
+  $ui->show_header();
+  echo '<h2>CLI only</h2>
+        <p>This script must be run from the command line.</p>';
+  $ui->show_footer();
+  exit;
+}
+
+// parse command line args
+foreach ( array('silent', 'driver', 'dbhost', 'dbuser', 'dbpasswd', 'dbname', 'db_prefix', 'user', 'pass', 'email', 'sitename', 'sitedesc', 'copyright', 'urlscheme', 'lang_id', 'scriptpath') as $var )
+{
+  if ( !isset($$var) )
+  {
+    $$var = false;
+  }
+}
+
+for ( $i = 1; $i < count($argv); $i++ )
+{
+  switch($argv[$i])
+  {
+    case '-q':
+      $silent = true;
+      break;
+    case '--db-driver':
+    case '-b':
+      $driver = @$argv[++$i];
+      break;
+    case '--db-host':
+    case '-h':
+      $dbhost = @$argv[++$i];
+      break;
+    case '--db-user':
+    case '-u':
+      $dbuser = @$argv[++$i];
+      break;
+    case '--db-pass':
+    case '-p':
+      $dbpasswd = @$argv[++$i];
+      break;
+    case '--db-name':
+    case '-d':
+      $dbname = @$argv[++$i];
+      break;
+    case '--table-prefix':
+    case '-t':
+      $db_prefix = @$argv[++$i];
+      break;
+    case '--admin-user':
+    case '-a':
+      $user = @$argv[++$i];
+      break;
+    case '--admin-pass':
+    case '-w':
+      $pass = @$argv[++$i];
+      break;
+    case '--admin-email':
+    case '-e':
+      $email = @$argv[++$i];
+      break;
+    case '--site-name':
+    case '-n':
+      $sitename = @$argv[++$i];
+      break;
+    case '--site-desc':
+    case '-s':
+      $sitedesc = @$argv[++$i];
+      break;
+    case '--copyright':
+    case '-c':
+      $copyright = @$argv[++$i];
+      break;
+    case '--url-scheme':
+    case '-r':
+      $urlscheme_temp = @$argv[++$i];
+      if ( in_array($urlscheme_temp, array('standard', 'short', 'rewrite')) )
+        $urlscheme = $urlscheme_temp;
+      break;
+    case '--language':
+    case '-l':
+      $lang_id = @$argv[++$i];
+      break;
+    case '-i':
+    case '--scriptpath':
+      $scriptpath = @$argv[++$i];
+      break;
+    default:
+      $vers = installer_enano_version();
+      echo <<<EOF
+Enano CMS v$vers - CLI Installer
+Usage: {$argv[0]} [-q] [-b driver] [-h host] [-u username] [-p password]
+                  [-d database] [-a adminuser] [-w adminpass] [-e email]
+All arguments are optional; missing information will be prompted for.
+  -q                Quiet mode (minimal output)
+  -b, --db-driver   Database driver (mysql or postgresql)
+  -h, --db-host     Hostname of database server
+  -u, --db-user     Username to use on database server
+  -p, --db-pass     Password to use on database server
+  -d, --db-name     Name of database
+  -a, --admin-user  Administrator username
+  -w, --admin-pass  Administrator password
+  -e, --admin-email Administrator e-mail address
+  -n, --site-name   Name of site
+  -s, --site-desc   *SHORT* Description of site
+  -c, --copyright   Copyright notice shown on pages
+  -r, --url-scheme  URL scheme (standard, short, or rewrite)
+  -l, --language    Language to be used on site and in installer
+  -i, --scriptpath  Where Enano is relative to your website root (no trailing
+                    slash)
+
+
+EOF;
+      exit(1);
+      break;
+  }
+}
+
+if ( $silent )
+{
+  define('ENANO_LIBINSTALL_SILENT', '');
+}
+
+##
+## PHP VERSION CHECK
+##
+
+if ( version_compare(PHP_VERSION, '5.0.0', '<' ) )
+{
+  if ( !$silent )
+  {
+    echo "\x1B[1mWelcome to the \x1B[34mEnano\x1B[0m CMS\x1B[1m installation wizard.\x1B[0m\n";
+    echo "Installing Enano version \x1B[1m" . installer_enano_version() . "\x1B[0m on PHP " . PHP_VERSION . "\n";
+  }
+  installer_fail('Your version of PHP (' . PHP_VERSION . ') doesn\'t meet Enano requirements (5.0.0)');
+}
+
+##
+## LANGUAGE STARTUP
+##
+
+// Include language lib and additional PHP5-only JSON functions
+require_once( ENANO_ROOT . '/includes/json2.php' );
+require_once( ENANO_ROOT . '/includes/lang.php' );
+
+// Determine language ID to use
+$langids = array_keys($languages);
+if ( $silent )
+{
+  if ( !in_array($lang_id, $langids ) )
+    $lang_id = $langids[0];
+}
+else if ( !in_array($lang_id, $langids) )
+{
+  echo "\x1B[1mPlease select a language.\x1B[0m\n";
+  echo "\x1B[32mAvailable languages:\x1B[0m\n";
+  foreach ( $languages as $id => $metadata )
+  {
+    $id_spaced = $id;
+    while ( strlen($id_spaced) < 10 )
+      $id_spaced = "$id_spaced ";
+    echo "  \x1B[1;34m$id_spaced\x1B[0m {$metadata['name']} ({$metadata['name_eng']})\n";
+  }
+  while ( !in_array($lang_id, $langids) )
+  {
+    $lang_id = cli_prompt('Language: ', $langids[0]);
+  }
+}
+
+// We have a language ID - init language
+$language_dir = $languages[$lang_id]['dir'];
+
+// Initialize language support
+$lang = new Language($lang_id);
+$lang->load_file(ENANO_ROOT . '/language/' . $language_dir . '/install.json');
+
+##
+## WELCOME MESSAGE
+##
+
+if ( !$silent )
+{
+  echo parse_shellcolor_string($lang->get('cli_welcome_line1'));
+  echo parse_shellcolor_string($lang->get('cli_welcome_line2', array('enano_version' => installer_enano_version(), 'php_version' => PHP_VERSION)));
+}
+
+$defaults = array(
+  'driver'  => 'mysql',
+  'dbhost'    => 'localhost',
+  'dbuser'    => false,
+  'dbpasswd'  => false,
+  'dbname'    => false,
+  'db_prefix'    => '',
+  'user'      => 'admin',
+  'pass'      => false,
+  'email'     => false,
+  'sitename'  => $lang->get('cli_default_site_name'),
+  'sitedesc'  => $lang->get('cli_default_site_desc'),
+  'copyright' => $lang->get('cli_default_copyright', array('year' => date('Y'))),
+  'urlscheme' => 'standard',
+  'scriptpath'=> '/enano'
+);
+
+$terms = array(
+  'driver'  => $lang->get('cli_prompt_driver'),
+  'dbhost'    => $lang->get('cli_prompt_dbhost'),
+  'dbuser'    => $lang->get('cli_prompt_dbuser'),
+  'dbpasswd'  => $lang->get('cli_prompt_dbpasswd'),
+  'dbname'    => $lang->get('cli_prompt_dbname'),
+  'db_prefix'    => $lang->get('cli_prompt_db_prefix'),
+  'user'      => $lang->get('cli_prompt_user'),
+  'pass'      => $lang->get('cli_prompt_pass'),
+  'email'     => $lang->get('cli_prompt_email'),
+  'sitename'  => $lang->get('cli_prompt_sitename'),
+  'sitedesc'  => $lang->get('cli_prompt_sitedesc'),
+  'copyright' => $lang->get('cli_prompt_copyright'),
+  'urlscheme' => $lang->get('cli_prompt_urlscheme'),
+  'scriptpath'=> $lang->get('cli_prompt_scriptpath')
+);
+
+foreach ( array('driver', 'dbhost', 'dbuser', 'dbpasswd', 'dbname', 'db_prefix', 'scriptpath', 'user', 'pass', 'email', 'sitename', 'sitedesc', 'copyright', 'urlscheme') as $var )
+{
+  if ( empty($$var) )
+  {
+    switch($var)
+    {
+      default:
+        $$var = cli_prompt($terms[$var], $defaults[$var]);
+        break;
+      case 'pass':
+      case 'dbpasswd':
+        if ( @file_exists('/bin/stty') && @is_executable('/bin/stty') )
+        {
+          exec('/bin/stty -echo');
+          while ( true )
+          {
+            $$var = cli_prompt($terms[$var], $defaults[$var]);
+            echo "\n";
+            $confirm = cli_prompt($lang->get('cli_prompt_confirm'), $defaults[$var]);
+            echo "\n";
+            if ( $$var === $confirm )
+              break;
+            else
+              echo parse_shellcolor_string($lang->get('cli_err_pass_no_match'));
+          }
+          exec('/bin/stty echo');
+        }
+        else
+        {
+          $$var = cli_prompt("{$terms[$var]} " . $lang->get('cli_msg_echo_warning'), $defaults[$var]);
+        }
+        break;
+      case 'urlscheme':
+        $temp = '';
+        while ( !in_array($temp, array('standard', 'short', 'rewrite')) )
+        {
+          $temp = cli_prompt($terms[$var], $defaults[$var]);
+        }
+        $$var = $temp;
+        break;
+      case 'db_prefix':
+        while ( !preg_match('/^[a-z0-9_]*$/', $$var) )
+        {
+          $$var = cli_prompt($terms[$var], $defaults[$var]);
+        }
+        break;
+    }
+  }
+}
+
+##
+## DB TEST
+##
+
+require( ENANO_ROOT . '/includes/dbal.php' );
+require( ENANO_ROOT . '/includes/sql_parse.php' );
+$dbal = new $driver();
+
+if ( !$silent )
+  echo parse_shellcolor_string($lang->get('cli_msg_testing_db'));
+
+$result = $dbal->connect(true, $dbhost, $dbuser, $dbpasswd, $dbname);
+if ( !$result )
+{
+  if ( !$silent )
+    echo parse_shellcolor_string($lang->get('cli_test_fail')) . "\n";
+  installer_fail($lang->get('cli_err_db_connect_fail'));
+}
+
+if ( !$silent )
+  echo parse_shellcolor_string($lang->get('cli_test_pass')) . "\n";
+
+##
+## SERVER REQUIREMENTS
+##
+
+if ( !$silent )
+{
+  echo parse_shellcolor_string($lang->get('cli_stage_sysreqs'));
+}
+
+$test_failed = false;
+
+run_test('return version_compare(\'5.2.0\', PHP_VERSION, \'<=\');', $lang->get('sysreqs_req_php5'), $lang->get('sysreqs_req_desc_php5'), true);
+run_test('return function_exists(\'mysql_connect\');', $lang->get('sysreqs_req_mysql'), $lang->get('sysreqs_req_desc_mysql'), true);
+run_test('return function_exists(\'pg_connect\');', $lang->get('sysreqs_req_postgres'), $lang->get('sysreqs_req_desc_postgres'), true);
+run_test('return @ini_get(\'file_uploads\');', $lang->get('sysreqs_req_uploads'), $lang->get('sysreqs_req_desc_uploads') );
+run_test('return config_write_test();', $lang->get('sysreqs_req_config'), $lang->get('sysreqs_req_desc_config') );
+run_test('return file_exists(\'/usr/bin/convert\');', $lang->get('sysreqs_req_magick'), $lang->get('sysreqs_req_desc_magick'), true);
+run_test('return is_writable(ENANO_ROOT.\'/cache/\');', $lang->get('sysreqs_req_cachewriteable'), $lang->get('sysreqs_req_desc_cachewriteable'), true);
+run_test('return is_writable(ENANO_ROOT.\'/files/\');', $lang->get('sysreqs_req_fileswriteable'), $lang->get('sysreqs_req_desc_fileswriteable'), true);
+if ( !function_exists('mysql_connect') && !function_exists('pg_connect') )
+{
+  installer_fail($lang->get('cli_err_no_drivers'));
+}
+if ( $test_failed )
+{
+  installer_fail($lang->get('cli_err_sysreqs_fail'));
+}
+
+##
+## STAGE 1 INSTALLATION
+##
+
+if ( !$silent )
+{
+  echo parse_shellcolor_string($lang->get('cli_msg_tests_passed'));
+  echo parse_shellcolor_string($lang->get('cli_msg_installing_db_stage1'));
+}
+
+// Create the config table
+try
+{
+  $sql_parser = new SQL_Parser( ENANO_ROOT . "/install/schemas/{$driver}_stage1.sql" );
+}
+catch ( Exception $e )
+{
+  if ( !$silent )
+    echo "\n";
+  installer_fail($lang->get('cli_err_schema_load'));
+}
+// Check to see if the config table already exists
+$q = $dbal->sql_query('SELECT config_name, config_value FROM ' . $db_prefix . 'config LIMIT 1;');
+if ( !$q )
+{
+  $sql_parser->assign_vars(array(
+      'TABLE_PREFIX' => $db_prefix
+    ));
+  $sql = $sql_parser->parse();
+  foreach ( $sql as $q )
+  {
+    if ( !$dbal->sql_query($q) )
+    {
+      if ( !$silent )
+        echo "\n";
+      echo "[$driver] " . $dbal->sql_error() . "\n";
+      installer_fail($lang->get('cli_err_db_query'));
+    }
+  }
+}
+else
+{
+  $dbal->free_result();
+  if ( !$dbal->sql_query('DELETE FROM ' . $db_prefix . 'config WHERE config_name = \'install_aes_key\';') )
+  {
+    if ( !$silent )
+      echo "\n";
+    echo "[$driver] " . $dbal->sql_error() . "\n";
+    installer_fail($lang->get('cli_err_db_query'));
+  }
+}
+
+if ( !$silent )
+  echo parse_shellcolor_string($lang->get('cli_msg_ok')) . "\n";
+
+define('table_prefix', $db_prefix);
+
+##
+## STAGE 2 INSTALLATION
+##
+
+$db =& $dbal;
+$dbdriver =& $driver;
+
+// Yes, I am predicting the future here. Because I have that kind of power.
+$_SERVER['REMOTE_ADDR'] = ( intval(date('Y')) >= 2011 ) ? '::1' : '127.0.0.1';
+
+if ( !$silent )
+  echo parse_shellcolor_string($lang->get('cli_msg_parsing_schema'));
+
+require_once( ENANO_ROOT . '/includes/rijndael.php' );
+require_once( ENANO_ROOT . '/includes/hmac.php' );
+
+$aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE);
+$hmac_secret = hexencode(AESCrypt::randkey(20), '', '');
+
+$admin_pass_clean =& $pass;
+$admin_pass = hmac_sha1($admin_pass_clean, $hmac_secret);
+
+unset($admin_pass_clean); // Security
+
+try
+{
+  $sql_parser = new SQL_Parser( ENANO_ROOT . "/install/schemas/{$dbdriver}_stage2.sql" );
+}
+catch ( Exception $e )
+{
+  if ( !$silent )
+    echo "\n";
+  installer_fail($lang->get('cli_err_schema_load'));
+}
+
+$wkt = ENANO_ROOT . "/language/{$languages[$lang_id]['dir']}/install/mainpage-default.wkt";
+if ( !file_exists( $wkt ) )
+{
+  if ( !$silent )
+    echo "\n";
+  installer_fail($lang->get('cli_err_mainpage_load'));
+}
+$wkt = @file_get_contents($wkt);
+if ( empty($wkt) )
+  return false;
+
+$wkt = $db->escape($wkt);
+
+$vars = array(
+    'TABLE_PREFIX'         => table_prefix,
+    'SITE_NAME'            => $sitename,
+    'SITE_DESC'            => $sitedesc,
+    'COPYRIGHT'            => $copyright,
+    'WIKI_MODE'            => '0',
+    'ENABLE_CACHE'         => ( is_writable( ENANO_ROOT . '/cache/' ) ? '1' : '0' ),
+    'VERSION'              => installer_enano_version(),
+    'ADMIN_USER'           => $db->escape($user),
+    'ADMIN_PASS'           => $admin_pass,
+    'ADMIN_PASS_SALT'      => $hmac_secret,
+    'ADMIN_EMAIL'          => $db->escape($email),
+    'REAL_NAME'            => '', // This has always been stubbed.
+    'ADMIN_EMBED_PHP'      => strval(AUTH_DISALLOW),
+    'UNIX_TIME'            => strval(time()),
+    'MAIN_PAGE_CONTENT'    => $wkt,
+    'IP_ADDRESS'           => $_SERVER['REMOTE_ADDR']
+  );
+
+$sql_parser->assign_vars($vars);
+$schema = $sql_parser->parse();
+
+if ( !$silent )
+  echo parse_shellcolor_string($lang->get('cli_msg_ok')) . "\n";
+
+##
+## PAYLOAD DELIVERY
+##
+
+if ( !$silent )
+  echo parse_shellcolor_string($lang->get('cli_msg_installing_db_stage2'));
+
+foreach ( $schema as $sql )
+{
+  if ( !$db->check_query($sql) )
+  {
+    if ( !$silent )
+      echo "\n";
+    installer_fail($lang->get('cli_err_query_sanity_failed'));
+  }
+}
+
+foreach ( $schema as $sql )
+{
+  if ( !$db->sql_query($sql) )
+  {
+    if ( !$silent )
+      echo "\n";
+    echo "[$dbdriver] " . $db->sql_error() . "\n";
+    installer_fail($lang->get('cli_err_db_query'));
+  }
+}
+
+if ( !$silent )
+  echo parse_shellcolor_string($lang->get('cli_msg_ok')) . "\n";
+
+##
+## CONFIG FILE GENERATION
+##
+
+require_once( ENANO_ROOT . '/install/includes/payload.php' );
+require_once( ENANO_ROOT . '/install/includes/libenanoinstallcli.php' );
+define('scriptPath', $scriptpath);
+$urlscheme = strtr($urlscheme, array(
+  'short' => 'shortened'
+));
+$_POST['url_scheme'] =& $urlscheme;
+
+run_installer_stage('writeconfig', 'writing_config', 'stg_write_config', 'install_stg_writeconfig_body');
+
+##
+## FINAL STAGES
+##
+
+if ( !$silent )
+  echo parse_shellcolor_string($lang->get('cli_msg_starting_api'));
+
+// Start up the Enano API
+$db->close();
+@define('ENANO_ALLOW_LOAD_NOLANG', 1);
+// If this fails, it fails hard.
+require(ENANO_ROOT . '/includes/common.php');
+
+if ( !$silent )
+  echo parse_shellcolor_string($lang->get('cli_msg_ok')) . "\n";
+
+run_installer_stage('importlang', 'importing_language', 'stg_language_setup', $lang->get('install_stg_importlang_body'));
+run_installer_stage('initlogs', 'initting_logs', 'stg_init_logs', $lang->get('install_stg_initlogs_body'));
+run_installer_stage('cleanup', 'cleaning_up', 'stg_aes_cleanup', $lang->get('install_stg_cleanup_body'), false);
+run_installer_stage('buildindex', 'initting_index', 'stg_build_index', $lang->get('install_stg_buildindex_body'));
+run_installer_stage('renameconfig', 'renaming_config', 'stg_rename_config', $lang->get('install_stg_rename_body', array('mainpage_link' => scriptPath . '/index.php')));
+
+if ( !$silent )
+{
+  echo parse_shellcolor_string($lang->get('cli_msg_install_success'));
+}
+
+return true;
+
+##
+## FUNCTIONS
+##
+
+function cli_prompt($prompt, $default = false)
+{
+  if ( is_string($default) )
+  {
+    echo "$prompt [$default]: ";
+    $stdin = fopen('php://stdin', 'r');
+    $input = trim(fgets($stdin, 1024));
+    fclose($stdin);
+    if ( empty($input) )
+      return $default;
+    return $input;
+  }
+  else
+  {
+    while ( true )
+    {
+      echo "$prompt: ";
+      $stdin = fopen('php://stdin', 'r');
+      $input = trim(fgets($stdin, 1024));
+      fclose($stdin);
+      if ( !empty($input) )
+        return $input;
+    }
+  }
+}
+
+function run_test($evalme, $test, $description, $warnonly = false)
+{
+  global $silent, $test_failed, $lang;
+  if ( !$silent )
+    echo "$test: ";
+  $result = eval($evalme);
+  if ( $result )
+  {
+    if ( !$silent )
+      echo parse_shellcolor_string($lang->get('cli_test_pass'));
+  }
+  else
+  {
+    if ( !$silent )
+      echo $warnonly ? parse_shellcolor_string($lang->get('cli_test_warn')) : parse_shellcolor_string($lang->get('cli_test_fail'));
+    if ( !$silent )
+      echo "\n" . preg_replace('/^/m', '  ', wordwrap(strip_tags($description)));
+    if ( !$warnonly )
+      $test_failed = true;
+  }
+  if ( !$silent )
+    echo "\n";
+}
+
+function installer_fail($message)
+{
+  global $silent;
+  if ( $silent )
+    file_put_contents('php://stderr', "$message\n");
+  else
+    echo "\x1B[1;31m" . "Error:\x1B[0;1m $message\x1B[0m\n";
+  exit(1);
+}
+
+function config_write_test()
+{
+  if ( !is_writable(ENANO_ROOT.'/config.new.php') )
+    return false;
+  // We need to actually _open_ the file to make sure it can be written, because sometimes this fails even when is_writable() returns
+  // true on Windows/IIS servers. Don't ask me why.
+  $h = @fopen( ENANO_ROOT . '/config.new.php', 'a+' );
+  if ( !$h )
+    return false;
+  fclose($h);
+  return true;
+}
+
+function parse_shellcolor_string($str)
+{
+  $expr = '/<c ((?:[0-9]+)(?:;[0-9]+)*)>([\w\W]*?)<\/c>/';
+  while ( preg_match($expr, $str) )
+    $str = preg_replace($expr, "\x1B[\\1m\\2\x1B[0m", $str);
+  
+  return $str;
+}
+
--- a/install/includes/common.php	Sun Jan 11 21:37:49 2009 -0500
+++ b/install/includes/common.php	Wed Jan 14 20:33:05 2009 -0500
@@ -155,4 +155,13 @@
 // List of available DB drivers
 $supported_drivers = array('mysql', 'postgresql');
 
+// Divert to CLI loader if running from CLI
+if ( isset($argc) && isset($argv) )
+{
+  if ( is_int($argc) && is_array($argv) && !isset($_SERVER['REQUEST_URI']) )
+  {
+    define('ENANO_CLI', '');
+  }
+}
+
 ?>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/install/includes/libenanoinstallcli.php	Wed Jan 14 20:33:05 2009 -0500
@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
+ * Version 1.1.6 (Caoineag beta 1)
+ * Copyright (C) 2006-2008 Dan Fuhry
+ * Installation package
+ * libenanoinstallcli.php - Installer frontend logic, CLI version
+ *
+ * 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.
+ */
+
+function run_installer_stage($stage_id, $stage_name, $function, $failure_explanation, $allow_skip = true)
+{
+  global $silent, $lang;
+  
+  if ( !$silent )
+    echo parse_shellcolor_string($lang->get("cli_msg_$stage_name"));
+  
+  $result = @call_user_func($function);
+  
+  if ( !$result )
+  {
+    if ( !$silent )
+      echo parse_shellcolor_string($lang->get('cli_test_fail')) . "\n";
+    installer_fail($lang->get("cli_err_$stage_name"));
+  }
+  
+  if ( !$silent )
+    echo parse_shellcolor_string($lang->get('cli_msg_ok')) . "\n";
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/install/install-cli.php	Wed Jan 14 20:33:05 2009 -0500
@@ -0,0 +1,21 @@
+#!/usr/bin/env php
+<?php
+
+/*
+ * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
+ * Version 1.1.6 (Caoineag beta 1)
+ * Copyright (C) 2006-2008 Dan Fuhry
+ * Installation package
+ * install-cli.php - CLI installation frontend stub
+ *
+ * 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.
+ * 
+ * Thanks to Stephan for helping out with l10n in the installer (his work is in includes/stages/*.php).
+ */
+
+require(dirname(__FILE__) . '/includes/cli-core.php');
+
--- a/language/english/install.json	Sun Jan 11 21:37:49 2009 -0500
+++ b/language/english/install.json	Wed Jan 14 20:33:05 2009 -0500
@@ -15,7 +15,7 @@
 
 var enano_lang_install = {
   categories: [
-    'meta', 'language', 'welcome', 'license', 'sysreqs', 'database', 'dbmysql', 'dbpgsql', 'website', 'login', 'confirm', 'install', 'finish', 'pophelp', 'upgrade'
+    'meta', 'language', 'welcome', 'license', 'sysreqs', 'database', 'dbmysql', 'dbpgsql', 'website', 'login', 'confirm', 'install', 'finish', 'pophelp', 'upgrade', 'cli'
   ],
   strings: {
     meta: {
@@ -84,7 +84,7 @@
       req_desc_mysql: 'It seems that your PHP installation does not have the MySQL extension enabled. If this is your own server, you may need to just enable the "libmysql.so" extension in php.ini. If you do not have the MySQL extension installed, you will need to either use your distribution\'s package manager to install it, or you will have to compile PHP from source. If you compile PHP from source, please remember to use the "--with-mysql" configure option, and you will have to have the MySQL development files installed (they usually are). If this is not your server, please contact your hosting company and ask them to install the PHP MySQL extension.',
       req_desc_uploads: 'It seems that your server does not support uploading files. Enano *requires* this functionality in order to work properly. Please ask your server administrator to set the "file_uploads" option in php.ini to "On".',
       req_desc_apache: 'Apparently your server is running a web server other than Apache. Enano will work nontheless, but there are some known bugs with non-Apache servers, and the "fancy" URLs will not work properly. The "Standard URLs" option will be set on the website configuration page, only change it if you are absolutely certain that your server is running Apache.',
-      req_desc_config: 'It looks like the configuration file, config.new.php, is not writable. Enano needs to be able to write to this file in order to install.<br /><br /><b>If you are installing Enano on a SourceForge web site:</b><br />SourceForge mounts the web partitions read-only now, so you will need to use the project shell service to symlink config.php to a file in the /tmp/persistent directory.',
+      req_desc_config: 'It looks like the configuration file, config.new.php, is not writable. Enano needs to be able to write to this file in order to install. <br /><br /><b>If you are installing Enano on a SourceForge web site:</b> <br />SourceForge mounts the web partitions read-only now, so you will need to use the project shell service to symlink config.php to a file in the /tmp/persistent directory.',
       req_desc_magick: 'Enano uses ImageMagick to scale images into thumbnails. Because ImageMagick was not found on your server, Enano will use the width= and height= attributes on the &lt;img&gt; tag to scale images. This can cause somewhat of a performance increase, but bandwidth usage will be higher, especially if you use high-resolution images on your site.<br /><br />If you are sure that you have ImageMagick, you can set the location of the "convert" program using the administration panel after installation is complete.',
       req_desc_cachewriteable: 'Apparently the cache/ directory is not writable. Enano will still work, but you will not be able to cache thumbnails, meaning the server will need to re-render them each time they are requested. In some cases, this can cause a significant slowdown.',
       req_desc_fileswriteable: 'It seems that the directory where uploaded files are stored (%enano_root%/files) cannot be written by the server. Enano will still function, but file uploads will not function, and will be disabled by default.',
@@ -399,6 +399,67 @@
       stg_flushcache_body: 'The upgrader failed to delete some cached data. You may experience some problems with file corruption or badly drawn pages until the caches expire, which is often no longer than 20 minutes.',
       stg_setversion_title: 'Set Enano version and log upgrade',
       stg_setversion_body: 'There was a problem finalizing the upgrade and inserting logs. You really shouldn\'t ever see this message, but calling setConfig(\'enano_version\', installer_enano_version()) should get you rolling again since everything else is probably done by now.',
+    },
+    cli: {
+      welcome_line1: '<c 1>Welcome to the <c 34>Enano</c></c> CMS<c 1> installation wizard.</c>\n',
+      welcome_line2: 'Installing Enano version <c 1>%enano_version%</c> on PHP <c 1>%php_version%</c>\n',
+      
+      prompt_dbdriver: 'Database type (mysql or postgresql)',
+      prompt_dbhost: 'Hostname of database server',
+      prompt_dbuser: 'Database username',
+      prompt_dbpasswd: 'Database password',
+      prompt_dbname: 'Database name',
+      prompt_tblpfx: 'Table prefix',
+      prompt_user: 'Enano administrator username',
+      prompt_pass: 'Enano administrator password',
+      prompt_email: 'Contact e-mail',
+      prompt_sitename: 'Site name',
+      prompt_sitedesc: 'Site description',
+      prompt_copyright: 'Copyright notice',
+      prompt_urlscheme: 'URL scheme (standard, short, or rewrite)',
+      prompt_scriptpath: 'Now we need the path to Enano, relative to your website\'s document root,\nwithout a trailing slash. This should be based on the URL your site will\nuse. For example, if your site is http://yoursite.com/enano/, use the value\n"/enano" here. Leave this blank if Enano is in your document root, e.g.\nhttp://yoursite.com/index.php?title=Enano_page.\nSite URL',
+      prompt_confirm: 'Enter again to confirm',
+      msg_echo_warning: '(WARNING, will be echoed)',
+      
+      default_site_name: 'Enano site',
+      default_site_desc: 'My first Enano site',
+      default_copyright: '&copy; %year%',
+      
+      msg_testing_db: 'Testing database connectivity...',
+      
+      stage_sysreqs: '<c 34;1>Checking system.</c>\n',
+      test_pass: '<c 1;32>PASS</c>',
+      test_warn: '<c 1;33>FAIL</c>',
+      test_fail: '<c 1;31>FAIL</c>',
+      
+      msg_tests_passed: '<c 32><c 1>All tests passed.</c> Installing Enano.</c>\n',
+      msg_installing_db_stage1: '<c 1>Installing database, stage 1...</c>',
+      msg_parsing_schema: '<c 1>Preparing installation payload...</c>',
+      msg_installing_db_stage2: '<c 1>Installing database, stage 2...</c>',
+      msg_writing_config: '<c 1>Generating config file...</c>',
+      msg_starting_api: '<c 1>Starting up the Enano API...</c>',
+      msg_importing_language: '<c 1>Importing default language...</c>',
+      msg_initting_logs: '<c 1>Initializing logs...</c>',
+      msg_cleaning_up: '<c 1>Removing temporary data...</c>',
+      msg_initting_index: '<c 1>Initializing search index...</c>',
+      msg_renaming_config: '<c 1>Renaming config file...</c>',
+      
+      msg_ok: '<c 1;32>OK</c>',
+      msg_install_success: '<c 1;32>Congratulations!</c> <c 1>Enano was successfully installed.</c>\n<c 34>Now, navigate your browser to your Enano installation to make sure everything\nworks right. If you encounter problems, contact the Enano Team for support at\nhttp://forum.enanocms.org or in #enano on irc.freenode.net.</c> <c 34;1>Be sure to\nmention that you used the CLI installer.</c>\n',
+      
+      err_pass_no_match: '  <c 1>Passwords do not match.</c>\n',
+      err_db_connect_fail: 'Could not connect to the database',
+      err_sysreqs_fail: 'One or more system requirement checks has failed.',
+      err_no_drivers: 'Support for at least one database driver is required.',
+      err_schema_load: 'Could not load the database schema file.',
+      err_db_query: 'A database error occurred; see the message above for details.',
+      err_query_sanity_failed: 'The sanity check on a database query failed.',
+      err_writing_config: 'Could not write the configuration file.',
+      err_importing_language: 'Could not import the default language.',
+      err_initting_logs: 'Could not initialize Enano\'s logs.',
+      err_cleaning_up: 'Could not clean up the temporary data.',
+      err_initting_index: 'Could not initialize the search index.',
+      err_renaming_config: 'Could not rename config.new.php to config.php; please do this manually.',
     }
   }
 }
--- a/plugins/admin/PluginManager.php	Sun Jan 11 21:37:49 2009 -0500
+++ b/plugins/admin/PluginManager.php	Wed Jan 14 20:33:05 2009 -0500
@@ -123,7 +123,7 @@
   if ( $paths->getParam(0) == 'action.json' )
   {
     // Set to application/json to discourage advertisement scripts
-    header('Content-Type: application/json');
+    header('Content-Type: text/javascript');
     
     // Init return data
     $return = array('mode' => 'error', 'error' => 'undefined');