A number of scattered changes. Profiler added and only enabled in debug mode (currently on), but awfully useful for fixing performance in the future. Started work on Admin:LangManager
authorDan
Thu, 24 Jan 2008 22:06:09 -0500
changeset 372 5bd429428101
parent 371 dc6026376919
child 373 5c6e09bc7d69
A number of scattered changes. Profiler added and only enabled in debug mode (currently on), but awfully useful for fixing performance in the future. Started work on Admin:LangManager
includes/clientside/static/misc.js
includes/common.php
includes/functions.php
includes/lang.php
includes/pageprocess.php
includes/pageutils.php
includes/paths.php
includes/sessions.php
includes/template.php
index.php
language/english/admin.json
language/english/user.json
plugins/SpecialAdmin.php
plugins/SpecialUpdownload.php
plugins/SpecialUserFuncs.php
plugins/SpecialUserPrefs.php
plugins/admin/LangManager.php
--- a/includes/clientside/static/misc.js	Wed Jan 23 12:48:22 2008 -0500
+++ b/includes/clientside/static/misc.js	Thu Jan 24 22:06:09 2008 -0500
@@ -325,7 +325,7 @@
     case 'invalid_credentials':
       $errstring = $lang.get('user_err_invalid_credentials');
       var subst = {
-        lockout_fails: $data.lockout_fails,
+        fails: $data.lockout_fails,
         lockout_threshold: $data.lockout_threshold,
         lockout_duration: $data.lockout_duration
       }
--- a/includes/common.php	Wed Jan 23 12:48:22 2008 -0500
+++ b/includes/common.php	Thu Jan 24 22:06:09 2008 -0500
@@ -121,6 +121,8 @@
 
 strip_magic_quotes_gpc();
 
+profiler_log('Files included and magic_quotes_gpc reversed if applicable');
+
 // Enano has five main components: the database abstraction layer (DBAL), the session manager,
 // the path/URL manager, the template engine, and the plugin manager.
 // Each part has its own class and a global object; nearly all Enano functions are handled by one of these five components.
@@ -163,6 +165,8 @@
 $db = new $dbdriver();
 $db->connect();
 
+profiler_log('Database connected');
+
 // The URL separator is the character appended to contentPath + url_title type strings.
 // If the contentPath has a ? in it, this should be an ampersand; else, it should be a
 // question mark.
@@ -203,6 +207,8 @@
 
 $db->free_result();
 
+profiler_log('Config fetched');
+
 // Now that we have the config, check the Enano version.
 if ( enano_version(false, true) != $version && !defined('IN_ENANO_UPGRADE') )
 {
@@ -240,7 +246,7 @@
 }
 
 // Is there no default language?
-if ( getConfig('lang_default') === false && !defined('IN_ENANO_MIGRATION') )
+if ( getConfig('default_language') === false && !defined('IN_ENANO_MIGRATION') )
 {
   $q = $db->sql_query('SELECT lang_id FROM '.table_prefix.'language LIMIT 1;');
   if ( !$q )
@@ -260,6 +266,8 @@
   setConfig('default_language', $row['lang_id']);
 }
 
+profiler_log('Ran checks');
+
 // Load plugin manager
 $plugins = new pluginLoader();
 
@@ -278,12 +286,16 @@
   include_once $f;
 }
 
+profiler_log('Loaded plugins');
+
 // Three fifths of the Enano API gets the breath of life right here.
 $session = new sessionManager();
 $paths = new pathManager();
 $template = new template();
 $email = new EmailEncryptor();
 
+profiler_log('Instanciated important singletons');
+
 // We've got the five main objects - flick on the switch so if a problem occurs, we can have a "friendly" UI
 define('ENANO_BASE_CLASSES_INITIALIZED', '');
 
@@ -301,6 +313,8 @@
     eval($cmd);
   }
   
+  profiler_log('Finished base_classes_initted hook');
+  
   // For special and administration pages, sometimes there is a "preloader" function that must be run
   // before the session manager and/or path manager get the init signal. Call it here.  
   $p = RenderMan::strToPageId($paths->get_pageid_from_url());
@@ -309,6 +323,8 @@
     @call_user_func('page_'.$p[1].'_'.$p[0].'_preloader');
   }
   
+  profiler_log('Checked for preloader');
+  
   // One quick security check...
   if ( !is_valid_ip($_SERVER['REMOTE_ADDR']) )
   {
@@ -328,6 +344,8 @@
     eval($cmd);
   }
   
+  profiler_log('Ran session_started hook');
+  
   $paths->init();
   
   // We're ready for whatever life throws us now.
@@ -369,10 +387,14 @@
     eval($cmd);
   }
   
+  profiler_log('Ran disabled-site checks and common_post');
+  
   if ( isset($_GET['noheaders']) )
     $template->no_headers = true;
 }
 
+profiler_log('common finished');
+
 // That's the end. Enano should be loaded now :-)
 
 ?>
--- a/includes/functions.php	Wed Jan 23 12:48:22 2008 -0500
+++ b/includes/functions.php	Thu Jan 24 22:06:09 2008 -0500
@@ -1292,7 +1292,8 @@
 function enano_debug_print_backtrace($return = false)
 {
   ob_start();
-  echo '<pre>';
+  if ( !$return )
+    echo '<pre>';
   if ( function_exists('debug_print_backtrace') )
   {
     debug_print_backtrace();
@@ -1301,7 +1302,8 @@
   {
     echo '<b>Warning:</b> No debug_print_backtrace() support!';
   }
-  echo '</pre>';
+  if ( !$return )
+    echo '</pre>';
   $c = ob_get_contents();
   ob_end_clean();
   if($return) return $c;
@@ -3244,7 +3246,7 @@
                          VALUES(
                            \'' . $db->escape($lang_code) . '\',
                            \'' . $db->escape($lang_name_neutral) . '\',
-                           \'' . $db->escape($lang_name_native) . '\'
+                           \'' . $db->escape($lang_name_local) . '\'
                          );');
   if ( !$q )
     $db->_die('functions.php - installing language');
@@ -3280,6 +3282,52 @@
 }
 
 /**
+ * Lists available languages.
+ * @return array Multi-depth. Associative, with children associative containing keys name, name_eng, and dir.
+ */
+
+function list_available_languages()
+{
+  // Pulled from install/includes/common.php
+  
+  // Build a list of available languages
+  $dir = @opendir( ENANO_ROOT . '/language' );
+  if ( !$dir )
+    die('CRITICAL: could not open language directory');
+  
+  $languages = array();
+  
+  while ( $dh = @readdir($dir) )
+  {
+    if ( $dh == '.' || $dh == '..' )
+      continue;
+    if ( file_exists( ENANO_ROOT . "/language/$dh/meta.json" ) )
+    {
+      // Found a language directory, determine metadata
+      $meta = @file_get_contents( ENANO_ROOT . "/language/$dh/meta.json" );
+      if ( empty($meta) )
+        // Could not read metadata file, continue silently
+        continue;
+        
+      // Do some syntax correction on the metadata
+      $meta = enano_clean_json($meta);
+        
+      $meta = enano_json_decode($meta);
+      if ( isset($meta['lang_name_english']) && isset($meta['lang_name_native']) && isset($meta['lang_code']) )
+      {
+        $languages[$meta['lang_code']] = array(
+            'name' => $meta['lang_name_native'],
+            'name_eng' => $meta['lang_name_english'],
+            'dir' => $dh
+          );
+      }
+    }
+  }
+  
+  return $languages;
+}
+
+/**
  * Scales an image to the specified width and height, and writes the output to the specified
  * file. Will use ImageMagick if present, but if not will attempt to scale with GD. This will
  * always scale images proportionally.
@@ -3893,6 +3941,140 @@
   return Zend_Json::decode($data, Zend_Json::TYPE_ARRAY);
 }
 
+/**
+ * Cleans a snippet of JSON for closer standards compliance (shuts up the picky Zend parser)
+ * @param string Dirty JSON
+ * @return string Clean JSON
+ */
+
+function enano_clean_json($json)
+{
+  // eliminate comments
+  $json = preg_replace(array(
+          // eliminate single line comments in '// ...' form
+          '#^\s*//(.+)$#m',
+          // eliminate multi-line comments in '/* ... */' form, at start of string
+          '#^\s*/\*(.+)\*/#Us',
+          // eliminate multi-line comments in '/* ... */' form, at end of string
+          '#/\*(.+)\*/\s*$#Us'
+        ), '', $json);
+    
+  $json = preg_replace('/([,\{\[])([\s]*?)([a-z0-9_]+)([\s]*?):/', '\\1\\2"\\3" :', $json);
+  
+  return $json;
+}
+
+/**
+ * Starts the profiler.
+ */
+
+function profiler_start()
+{
+  global $_profiler;
+  $_profiler = array();
+  
+  if ( !defined('ENANO_DEBUG') )
+    return false;
+  
+  $_profiler[] = array(
+      'point' => 'Profiling started',
+      'time' => microtime_float(),
+      'backtrace' => false
+    );
+}
+
+/**
+ * Logs something in the profiler.
+ * @param string Point name or message
+ * @param bool Optional. If true (default), a backtrace will be generated and added to the profiler data. False disables this, often for security reasons.
+ */
+
+function profiler_log($point, $allow_backtrace = true)
+{
+  if ( !defined('ENANO_DEBUG') )
+    return false;
+  
+  global $_profiler;
+  $backtrace = false;
+  if ( $allow_backtrace && function_exists('debug_print_backtrace') )
+  {
+    list(, $backtrace) = explode("\n", enano_debug_print_backtrace(true));
+  }
+  $_profiler[] = array(
+      'point' => $point,
+      'time' => microtime_float(),
+      'backtrace' => $backtrace
+    );
+}
+
+/**
+ * Returns the profiler's data (so far).
+ * @return array
+ */
+
+function profiler_dump()
+{
+  return $GLOBALS['_profiler'];
+}
+
+/**
+ * Generates an HTML version of the performance profile. Not localized because only used as a debugging tool.
+ * @return string
+ */
+
+function profiler_make_html()
+{
+  if ( !defined('ENANO_DEBUG') )
+    return '';
+    
+  $profile = profiler_dump();
+  
+  $html = '<div class="tblholder">';
+  $html .= '<table border="0" cellspacing="1" cellpadding="4">';
+  
+  $time_start = $time_last = $profile[0]['time'];
+  
+  foreach ( $profile as $i => $entry )
+  {
+    $html .= "<tr><th colspan=\"2\">Event $i</th></tr>";
+    
+    $html .= '<tr>';
+    $html .= '<td class="row2">Event:</td>';
+    $html .= '<td class="row1">' . htmlspecialchars($entry['point']) . '</td>';
+    $html .= '</tr>';
+    
+    $time = $entry['time'] - $time_start;
+    
+    $html .= '<tr>';
+    $html .= '<td class="row2">Time since start:</td>';
+    $html .= '<td class="row1">' . $time . 's</td>';
+    $html .= '</tr>';
+    
+    $time = $entry['time'] - $time_last;
+    
+    $html .= '<tr>';
+    $html .= '<td class="row2">Time since last event:</td>';
+    $html .= '<td class="row1">' . $time . 's</td>';
+    $html .= '</tr>';
+    
+    if ( $entry['backtrace'] )
+    {
+      $html .= '<tr>';
+      $html .= '<td class="row2">Called from:</td>';
+      $html .= '<td class="row1">' . htmlspecialchars($entry['backtrace']) . '</td>';
+      $html .= '</tr>';
+    }
+    
+    $time_last = $entry['time'];
+  }
+  $html .= '</table></div>';
+  
+  return $html;
+}
+
+// Might as well start the profiler, it has no external dependencies except from this file.
+profiler_start();
+
 //die('<pre>Original:  01010101010100101010100101010101011010'."\nProcessed: ".uncompress_bitfield(compress_bitfield('01010101010100101010100101010101011010')).'</pre>');
 
 ?>
--- a/includes/lang.php	Wed Jan 23 12:48:22 2008 -0500
+++ b/includes/lang.php	Thu Jan 24 22:06:09 2008 -0500
@@ -89,7 +89,8 @@
     }
     
     $lang_default = ( $x = getConfig('default_language') ) ? intval($x) : '\'def\'';
-    $q = $db->sql_query("SELECT lang_id, lang_code, last_changed, ( lang_id = $lang_default ) AS is_default FROM " . table_prefix . "language WHERE $sql_col OR lang_id = $lang_default ORDER BY is_default DESC LIMIT 1;");
+    
+    $q = $db->sql_query("SELECT lang_id, lang_code, last_changed, ( lang_id = $lang_default ) AS is_default FROM " . table_prefix . "language WHERE $sql_col OR lang_id = $lang_default ORDER BY is_default ASC LIMIT 1;");
     
     if ( !$q )
       $db->_die('lang.php - main select query');
@@ -320,18 +321,7 @@
     $contents = preg_replace('/\}([^}]+)$/', '}', $contents);
     
     // Correct syntax to be nice to the json parser
-    
-    // eliminate comments
-    $contents = preg_replace(array(
-            // eliminate single line comments in '// ...' form
-            '#^\s*//(.+)$#m',
-            // eliminate multi-line comments in '/* ... */' form, at start of string
-            '#^\s*/\*(.+)\*/#Us',
-            // eliminate multi-line comments in '/* ... */' form, at end of string
-            '#/\*(.+)\*/\s*$#Us'
-          ), '', $contents);
-    
-    $contents = preg_replace('/([,\{\[])([\s]*?)([a-z0-9_]+)([\s]*?):/', '\\1\\2"\\3" :', $contents);
+    $contents = enano_clean_json($contents);
     
     try
     {
--- a/includes/pageprocess.php	Wed Jan 23 12:48:22 2008 -0500
+++ b/includes/pageprocess.php	Thu Jan 24 22:06:09 2008 -0500
@@ -131,6 +131,8 @@
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     
+    profiler_log("PageProcessor [{$namespace}:{$page_id}]: Started constructor");
+    
     // See if we can get some debug info
     if ( function_exists('debug_backtrace') && $this->debug['enable'] )
     {
@@ -148,8 +150,9 @@
     if ( !is_int($revision_id) )
       $revision_id = 0;
     
+    profiler_log("PageProcessor [{$namespace}:{$page_id}]: Ran initial checks");
+    
     $this->_setup( $page_id, $namespace, $revision_id );
-    
   }
   
   /**
@@ -162,9 +165,12 @@
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $lang;
     
+    profiler_log("PageProcessor [{$this->namespace}:{$this->page_id}]: Started send process");
+    
     if ( !$this->perms->get_permissions('read') )
     {
       $this->err_access_denied();
+      profiler_log("PageProcessor [{$this->namespace}:{$this->page_id}]: Finished send process");
       return false;
     }
     $pathskey = $paths->nslist[ $this->namespace ] . $this->page_id;
@@ -193,6 +199,7 @@
           if ( $this->password != $password )
           {
             $this->err_wrong_password();
+            profiler_log("PageProcessor [{$this->namespace}:{$this->page_id}]: Finished send process");
             return false;
           }
         }
@@ -211,7 +218,10 @@
       $func_name = "page_{$this->namespace}_{$this->page_id}";
       if ( function_exists($func_name) )
       {
-        return @call_user_func($func_name);
+        profiler_log("PageProcessor [{$this->namespace}:{$this->page_id}]: Calling special/admin page");
+        $result = @call_user_func($func_name);
+        profiler_log("PageProcessor [{$this->namespace}:{$this->page_id}]: Finished send process");
+        return $result;
       }
       else
       {
@@ -230,6 +240,7 @@
           echo "<h2>$title</h2>
                 <p>$message</p>";
         }
+        profiler_log("PageProcessor [{$this->namespace}:{$this->page_id}]: Finished send process");
         return false;
       }
     }
@@ -297,6 +308,7 @@
       if ( $text == 'err_no_text_rows' )
       {
         $this->err_no_rows();
+        profiler_log("PageProcessor [{$this->namespace}:{$this->page_id}]: Finished send process");
         return false;
       }
       else
@@ -327,6 +339,7 @@
         }
       }
     }
+    profiler_log("PageProcessor [{$this->namespace}:{$this->page_id}]: Finished send process");
   }
   
   /**
@@ -596,6 +609,7 @@
     
     $this->title = get_page_title_ns($this->page_id, $this->namespace);
     
+    profiler_log("PageProcessor [{$this->namespace}:{$this->page_id}]: Ran _setup()");
   }
   
   /**
--- a/includes/pageutils.php	Wed Jan 23 12:48:22 2008 -0500
+++ b/includes/pageutils.php	Thu Jan 24 22:06:09 2008 -0500
@@ -21,7 +21,7 @@
    * @return string
    */
   
-  function checkusername($name)
+  public static function checkusername($name)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     $name = str_replace('_', ' ', $name);
@@ -47,7 +47,7 @@
    * @todo (DONE) Make it require a password (just for security purposes)
    */
    
-  function getsource($page, $password = false)
+  public static function getsource($page, $password = false)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     if(!isset($paths->pages[$page]))
@@ -95,7 +95,7 @@
    * @return string
    */
   
-  function getpage($page, $send_headers = false, $hist_id = false)
+  public static function getpage($page, $send_headers = false, $hist_id = false)
   {
     die('PageUtils->getpage is deprecated.');
     global $db, $session, $paths, $template, $plugins; // Common objects
@@ -328,7 +328,7 @@
    * @return string
    */
    
-  function savepage($page_id, $namespace, $message, $summary = 'No edit summary given', $minor = false)
+  public static function savepage($page_id, $namespace, $message, $summary = 'No edit summary given', $minor = false)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     $uid = sha1(microtime());
@@ -399,7 +399,7 @@
    * @return bool true on success, false on failure
    */
   
-  function createPage($page_id, $namespace, $name = false, $visible = 1)
+  public static function createPage($page_id, $namespace, $name = false, $visible = 1)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     if(in_array($namespace, Array('Special', 'Admin')))
@@ -494,7 +494,7 @@
    * @param $reason string why the page is being (un)protected
    * @return string - "good" on success, in all other cases, an error string (on query failure, calls $db->_die() )
    */
-  function protect($page_id, $namespace, $level, $reason)
+  public static function protect($page_id, $namespace, $level, $reason)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     
@@ -548,7 +548,7 @@
    * @return string
    */
   
-  function histlist($page_id, $namespace)
+  public static function histlist($page_id, $namespace)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $lang;
@@ -741,7 +741,7 @@
    * @return string
    */
    
-  function rollback($id)
+  public static function rollback($id)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     if ( !$session->get_permissions('history_rollback') )
@@ -908,7 +908,7 @@
    * @return string javascript code
    */
    
-  function addcomment($page_id, $namespace, $name, $subject, $text, $captcha_code = false, $captcha_id = false)
+  public static function addcomment($page_id, $namespace, $name, $subject, $text, $captcha_code = false, $captcha_id = false)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     $_ob = '';
@@ -943,7 +943,7 @@
    * @access private
    */
    
-  function comments_raw($page_id, $namespace, $action = false, $flags = Array(), $_ob = '')
+  public static function comments_raw($page_id, $namespace, $action = false, $flags = Array(), $_ob = '')
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $lang;
@@ -1202,7 +1202,7 @@
    * @return string
    */
    
-  function comments($page_id, $namespace, $action = false, $id = -1, $_ob = '')
+  public static function comments($page_id, $namespace, $action = false, $id = -1, $_ob = '')
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     $r = PageUtils::comments_raw($page_id, $namespace, $action, $id, $_ob);
@@ -1219,7 +1219,7 @@
    * @return string
    */
   
-  function comments_html($page_id, $namespace, $action = false, $id = -1, $_ob = '')
+  public static function comments_html($page_id, $namespace, $action = false, $id = -1, $_ob = '')
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     $r = PageUtils::comments_raw($page_id, $namespace, $action, $id, $_ob);
@@ -1238,7 +1238,7 @@
    * @return string
    */
   
-  function savecomment($page_id, $namespace, $subject, $text, $old_subject, $old_text, $id = -1)
+  public static function savecomment($page_id, $namespace, $subject, $text, $old_subject, $old_text, $id = -1)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     if(!$session->get_permissions('edit_comments'))
@@ -1289,7 +1289,7 @@
    * @return string
    */
   
-  function savecomment_neater($page_id, $namespace, $subject, $text, $id)
+  public static function savecomment_neater($page_id, $namespace, $subject, $text, $id)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     if(!is_int($id)) die('PageUtils::savecomment: $id is not an integer, aborting for safety');
@@ -1330,7 +1330,7 @@
    * @return string
    */
   
-  function deletecomment($page_id, $namespace, $name, $subj, $text, $id)
+  public static function deletecomment($page_id, $namespace, $name, $subj, $text, $id)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     
@@ -1367,7 +1367,7 @@
    * @return string
    */
   
-  function deletecomment_neater($page_id, $namespace, $id)
+  public static function deletecomment_neater($page_id, $namespace, $id)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     
@@ -1401,7 +1401,7 @@
    * @return string error string or success message
    */
    
-  function rename($page_id, $namespace, $name)
+  public static function rename($page_id, $namespace, $name)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $lang;
@@ -1449,7 +1449,7 @@
    * @return string error/success string
    */
    
-  function flushlogs($page_id, $namespace)
+  public static function flushlogs($page_id, $namespace)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $lang;
@@ -1488,7 +1488,7 @@
    * @return string
    */
    
-  function deletepage($page_id, $namespace, $reason)
+  public static function deletepage($page_id, $namespace, $reason)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $lang;
@@ -1521,7 +1521,7 @@
    * @return string
    */
    
-  function delvote($page_id, $namespace)
+  public static function delvote($page_id, $namespace)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $lang;
@@ -1588,7 +1588,7 @@
    * @return string
    */
   
-  function resetdelvotes($page_id, $namespace)
+  public static function resetdelvotes($page_id, $namespace)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $lang;
@@ -1611,7 +1611,7 @@
    * @return string JSON string with an array containing a list of themes
    */
    
-  function getstyles()
+  public static function getstyles()
   {
     
     if ( !preg_match('/^([a-z0-9_-]+)$/', $_GET['id']) )
@@ -1648,7 +1648,7 @@
    * @return string Javascript code
    */
    
-  function catedit($page_id, $namespace)
+  public static function catedit($page_id, $namespace)
   {
     $d = PageUtils::catedit_raw($page_id, $namespace);
     return $d[0] . ' /* BEGIN CONTENT */ document.getElementById("ajaxEditContainer").innerHTML = unescape(\''.rawurlencode($d[1]).'\');';
@@ -1659,7 +1659,7 @@
    * @access private
    */
    
-  function catedit_raw($page_id, $namespace)
+  public static function catedit_raw($page_id, $namespace)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $lang;
@@ -1742,7 +1742,7 @@
    * @return string "GOOD" on success, error string on failure
    */
   
-  function catsave($page_id, $namespace, $which_cats)
+  public static function catsave($page_id, $namespace, $which_cats)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     if(!$session->get_permissions('edit_cat')) return('Insufficient privileges to change category information');
@@ -1814,7 +1814,7 @@
    * @return string "GOOD" on success, error string on failure
    */
   
-  function setwikimode($page_id, $namespace, $level)
+  public static function setwikimode($page_id, $namespace, $level)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     if(!$session->get_permissions('set_wiki_mode')) return('Insufficient access rights');
@@ -1838,7 +1838,7 @@
    * @return string
    */
   
-  function setpass($page_id, $namespace, $pass)
+  public static function setpass($page_id, $namespace, $pass)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $lang;
@@ -1880,7 +1880,7 @@
    * @return string
    */
    
-  function genPreview($text)
+  public static function genPreview($text)
   {
     global $lang;
     $ret = '<div class="info-box">' . $lang->get('editor_preview_blurb') . '</div><div style="background-color: #F8F8F8; padding: 10px; border: 1px dashed #406080; max-height: 250px; overflow: auto; margin: 10px 0;">';
@@ -1901,7 +1901,7 @@
    * @return string
    */
    
-  function scrollBox($text, $height = 250)
+  public static function scrollBox($text, $height = 250)
   {
     return '<div style="background-color: #F8F8F8; padding: 10px; border: 1px dashed #406080; max-height: '.(string)intval($height).'px; overflow: auto; margin: 1em 0 1em 1em;">' . $text . '</div>';
   }
@@ -1915,7 +1915,7 @@
    * @return string XHTML-formatted diff
    */
    
-  function pagediff($page_id, $namespace, $id1, $id2)
+  public static function pagediff($page_id, $namespace, $id1, $id2)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $lang;
@@ -1952,7 +1952,7 @@
    * @return array
    */
    
-  function acl_editor($parms = Array())
+  public static function acl_editor($parms = Array())
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $lang;
@@ -2194,7 +2194,7 @@
    * @return string
    */
    
-  function acl_json($parms = '{ }')
+  public static function acl_json($parms = '{ }')
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     $parms = enano_json_decode($parms);
@@ -2208,7 +2208,7 @@
    * @param array The request data, if any, this should be in the format required by PageUtils::acl_editor()
    */
    
-  function aclmanager($parms)
+  public static function aclmanager($parms)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $lang;
@@ -2400,7 +2400,7 @@
    * @access private
    */
    
-  function acl_preprocess($parms)
+  public static function acl_preprocess($parms)
   {
     if ( !isset($parms['mode']) )
       // Nothing to do
@@ -2445,7 +2445,7 @@
     return $parms;
   }
   
-  function acl_postprocess($response)
+  public static function acl_postprocess($response)
   {
     if(!isset($response['mode']))
     {
--- a/includes/paths.php	Wed Jan 23 12:48:22 2008 -0500
+++ b/includes/paths.php	Thu Jan 24 22:06:09 2008 -0500
@@ -81,6 +81,7 @@
     $this->addAdminNode('adm_cat_general',    'adm_page_file_types',     'UploadAllowedMimeTypes');
     $this->addAdminNode('adm_cat_general',    'adm_page_plugins',        'PluginManager');
     $this->addAdminNode('adm_cat_general',    'adm_page_db_backup',      'DBBackup');
+    $this->addAdminNode('adm_cat_general',    'adm_page_lang_manager',   'LangManager');
     $this->addAdminNode('adm_cat_content',    'adm_page_manager',        'PageManager');
     $this->addAdminNode('adm_cat_content',    'adm_page_editor',         'PageEditor');
     $this->addAdminNode('adm_cat_content',    'adm_page_pg_groups',      'PageGroups');
@@ -410,6 +411,7 @@
     }
     
     $session->init_permissions();
+    profiler_log('Paths and CMS core initted');
   }
   
   function add_page($flags)
--- a/includes/sessions.php	Wed Jan 23 12:48:22 2008 -0500
+++ b/includes/sessions.php	Thu Jan 24 22:06:09 2008 -0500
@@ -560,6 +560,7 @@
       $this->style = 'default';
     }
     
+    profiler_log('Sessions started');
   }
   
   # Logins
@@ -678,7 +679,7 @@
       {
         $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
         // increment fail count
-        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');');
+        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\');');
         $fails++;
         // ooh boy, somebody's in trouble ;-)
         return array(
@@ -791,7 +792,7 @@
       {
         $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
         // increment fail count
-        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');');
+        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\');');
         $fails++;
         return array(
             'success' => false,
@@ -897,7 +898,7 @@
       {
         $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
         // increment fail count
-        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');');
+        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\');');
         $fails++;
         return array(
             'success' => false,
@@ -1002,7 +1003,7 @@
       {
         $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
         // increment fail count
-        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');');
+        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\');');
         $fails++;
         return array(
             'success' => false,
--- a/includes/template.php	Wed Jan 23 12:48:22 2008 -0500
+++ b/includes/template.php	Thu Jan 24 22:06:09 2008 -0500
@@ -132,6 +132,8 @@
     global $email;
     global $lang;
     
+    profiler_log("template: starting var init");
+    
     if(!$this->theme || !$this->style)
     {
       $this->load_theme();
@@ -791,6 +793,8 @@
     {
       eval($cmd);
     }
+    
+    profiler_log("template: finished var init");
   }
   
   function header($simple = false) 
@@ -834,10 +838,12 @@
             </div>';
     }
   }
+  
   function footer($simple = false)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
-    if(!$this->no_headers) {
+    if ( !$this->no_headers )
+    {
       
       if(!defined('ENANO_HEADERS_SENT'))
         $this->header();
@@ -858,12 +864,20 @@
       $t = str_replace('[[Stats]]', $dbg, $t);
       $t = str_replace('[[NumQueries]]', (string)$db->num_queries, $t);
       $t = str_replace('[[GenTime]]', (string)$f, $t);
+      
+      if ( defined('ENANO_DEBUG') )
+        $t = str_replace('</body>', '<div id="profile" style="margin: 10px;">' . profiler_make_html() . '</div></body>', $t);
+      
       echo $t;
       
       ob_end_flush();
     }
-    else return '';
+    else
+    {
+      return '';
+    }
   }
+  
   function getHeader()
   {
     $headers_sent = true;
--- a/index.php	Wed Jan 23 12:48:22 2008 -0500
+++ b/index.php	Thu Jan 24 22:06:09 2008 -0500
@@ -17,6 +17,9 @@
  */
  
   define('ENANO_INTERFACE_INDEX', '');
+  
+  // For the mighty and brave.
+  define('ENANO_DEBUG', '');
  
   // Set up gzip encoding before any output is sent
   
@@ -34,15 +37,6 @@
   
   error_reporting(E_ALL);
   
-  // if(!strstr($v, 'CSS') && !strstr($v, 'UploadFile') && !strstr($v, 'DownloadFile')) // These pages are blacklisted because we can't have debugConsole's HTML output disrupting the flow of header() calls and whatnot
-  // {
-  //   $do_gzip = ( function_exists('gzcompress') && ( isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strstr($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') ) ) ? true : false;
-  //   // Uncomment the following line to enable debugConsole (requires PHP 5 or later)
-  //   // define('ENANO_DEBUG', '');
-  // }
-  
-  if(defined('ENANO_DEBUG')) $do_gzip = false;
-  
   if($aggressive_optimize_html || $do_gzip)
   {
     ob_start();
--- a/language/english/admin.json	Wed Jan 23 12:48:22 2008 -0500
+++ b/language/english/admin.json	Thu Jan 24 22:06:09 2008 -0500
@@ -13,8 +13,8 @@
 var enano_lang = {
   categories: [
     'meta', 'adm', 'acl', 'adminusers',
-    'acphome', 'acpgc', 'acpup', 'acpft', 'acppl', 'acppm', 'acped', 'acpdb', 'acppg', 'acpum', 'acpug', 'acpcp', 'acpmm', 'acpsl', 'acpbc',
-    'acplo', 'sbedit',
+    'acphome', 'acpgc', 'acpup', 'acpft', 'acppl', 'acppm', 'acped', 'acpdb', 'acplm', 'acppg', 'acpum', 'acpug', 'acpcp', 'acpmm', 'acpsl',
+    'acpbc', 'acplo', 'sbedit',
   ],
   strings: {
     meta: {
@@ -26,6 +26,7 @@
       acpft: 'ACP: Allowed file types',
       acppl: 'ACP: Manage plugins',
       acpdb: 'ACP: Database backup',
+      acplm: 'ACP: Language manager',
       acppm: 'ACP: Manage pages',
       acped: 'ACP: Edit page content',
       acppg: 'ACP: Page groups',
@@ -51,6 +52,7 @@
       page_file_types: 'Allowed file types',
       page_plugins: 'Manage plugins',
       page_db_backup: 'Backup database',
+      page_lang_manager: 'Language manager',
       
       page_manager: 'Manage pages',
       page_editor: 'Edit page content',
@@ -440,6 +442,14 @@
       lbl_include_data: 'Include table data',
       btn_create_backup: 'Create backup',
     },
+    acplm: {
+      heading_install: 'Languages available for installation',
+      col_lang_code: 'ID',
+      col_lang_name: 'Language name (native)',
+      col_lang_name_eng: 'Language name (English)',
+      btn_install_language: 'Install',
+      msg_lang_install_success: 'The language pack %lang_name% has been installed.',
+    },
     acppg: {
       // Main menu
       heading_main: 'Manage page groups',
--- a/language/english/user.json	Wed Jan 23 12:48:22 2008 -0500
+++ b/language/english/user.json	Thu Jan 24 22:06:09 2008 -0500
@@ -242,6 +242,8 @@
       publicinfo_heading_main: 'Your public profile',
       publicinfo_note_optional: 'Please note that all of the information you enter here will be <b>publicly viewable.</b> All of the fields on this page are optional and may be left blank if you so desire.',
       publicinfo_field_realname: 'Real name:',
+      publicinfo_field_language: 'Preferred language:',
+      publicinfo_field_language_hint: 'Select the language special pages and page controls should appear in.',
       publicinfo_field_changetheme_title: 'Change theme:',
       publicinfo_field_changetheme_hint: 'If you don\'t like the look of the site, need a visual break, or are just curious, we might have some different themes for you to try out!',
       publicinfo_field_changetheme: 'Change my theme...',
--- a/plugins/SpecialAdmin.php	Wed Jan 23 12:48:22 2008 -0500
+++ b/plugins/SpecialAdmin.php	Thu Jan 24 22:06:09 2008 -0500
@@ -46,6 +46,7 @@
 require(ENANO_ROOT . '/plugins/admin/GroupManager.php');
 require(ENANO_ROOT . '/plugins/admin/SecurityLog.php');
 require(ENANO_ROOT . '/plugins/admin/UserManager.php');
+require(ENANO_ROOT . '/plugins/admin/LangManager.php');
 
 // function names are IMPORTANT!!! The name pattern is: page_<namespace ID>_<page URLname, without namespace>
 
--- a/plugins/SpecialUpdownload.php	Wed Jan 23 12:48:22 2008 -0500
+++ b/plugins/SpecialUpdownload.php	Thu Jan 24 22:06:09 2008 -0500
@@ -70,7 +70,7 @@
     }
     
     $types = fetch_allowed_extensions();
-    $ext = substr($file['name'], strrpos($file['name'], '.')+1, strlen($file['name']));
+    $ext = strtolower(substr($file['name'], strrpos($file['name'], '.')+1, strlen($file['name'])));
     if ( !isset($types[$ext]) || ( isset($types[$ext]) && !$types[$ext] ) )
     {
       die_friendly($lang->get('upload_err_title'), '<p>' . $lang->get('upload_err_banned_ext', array('ext' => htmlspecialchars($ext))) . '</p>');
--- a/plugins/SpecialUserFuncs.php	Wed Jan 23 12:48:22 2008 -0500
+++ b/plugins/SpecialUserFuncs.php	Thu Jan 24 22:06:09 2008 -0500
@@ -373,14 +373,6 @@
     $captcha_code = ( isset($data['captcha_code']) ) ? $data['captcha_code'] : false;
     $level = ( isset($data['level']) ) ? intval($data['level']) : USER_LEVEL_MEMBER;
     $result = $session->login_with_crypto($data['username'], $data['crypt_data'], $data['crypt_key'], $data['challenge'], $level, $captcha_hash, $captcha_code);
-    $session->start();
-    
-    // Run the session_started hook to establish special pages
-    $code = $plugins->setHook('session_started');
-    foreach ( $code as $cmd )
-    {
-      eval($cmd);
-    }
     
     if ( $result['success'] )
     {
@@ -419,18 +411,11 @@
     {
       $result = $session->login_without_crypto($_POST['username'], $_POST['pass'], false, intval($_POST['auth_level']), $captcha_hash, $captcha_code);
     }
-    $session->start();
-    
-    // Run the session_started hook to establish special pages
-    $code = $plugins->setHook('session_started');
-    foreach ( $code as $cmd )
-    {
-      eval($cmd);
-    }
-    
-    $paths->init();
+   
     if($result['success'])
     {
+      $session->start();
+      
       $template->load_theme($session->theme, $session->style);
       if(isset($_POST['return_to']))
       {
--- a/plugins/SpecialUserPrefs.php	Wed Jan 23 12:48:22 2008 -0500
+++ b/plugins/SpecialUserPrefs.php	Thu Jan 24 22:06:09 2008 -0500
@@ -547,8 +547,50 @@
         if ( !$q )
           $db->_die();
         
+        // verify language id
+        $lang_id = strval(intval($_POST['lang_id']));
+        $q = $db->sql_query('SELECT 1 FROM ' . table_prefix . 'language WHERE lang_id = ' . $lang_id . ';');
+        if ( !$q )
+          $db->_die();
+        
+        if ( $db->numrows() > 0 )
+        {
+          $db->free_result();
+          
+          // unload / reload $lang, this verifies that the selected language works
+          unset($GLOBALS['lang']);
+          unset($lang);
+          $lang_id = intval($lang_id);
+          $GLOBALS['lang'] = new Language($lang_id);
+          global $lang;
+          
+          $q = $db->sql_query('UPDATE ' . table_prefix . 'users SET user_lang = ' . $lang_id . " WHERE user_id = {$session->user_id};");
+          if ( !$q )
+            $db->_die();
+        }
+        else
+        {
+          $db->free_result();
+        }
+        
         echo '<div class="info-box" style="margin: 0 0 10px 0;">' . $lang->get('usercp_publicinfo_msg_save_success') . '</div>';
       }
+      
+      $lang_box = '<select name="lang_id">';
+      $q = $db->sql_query('SELECT lang_id, lang_name_native FROM ' . table_prefix . "language;");
+      if ( !$q )
+        $db->_die();
+      
+      while ( $row = $db->fetchrow_num() )
+      {
+        list($lang_id, $lang_name) = $row;
+        $lang_name = htmlspecialchars($lang_name);
+        $selected = ( $lang->lang_id == $lang_id ) ? ' selected="selected"' : '';
+        $lang_box .= "<option value=\"$lang_id\"$selected>$lang_name</option>";
+      }
+      
+      $lang_box .= '</select>';
+      
       echo '<form action="'.makeUrl($paths->fullpage).'" method="post">';
       ?>
       <div class="tblholder">
@@ -564,6 +606,10 @@
             <td class="row1" style="width: 50%;"><input type="text" name="real_name" value="<?php echo $session->real_name; ?>" size="30" /></td>
           </tr>
           <tr>
+            <td class="row2"><?php echo $lang->get('usercp_publicinfo_field_language') . '<br /><small>' . $lang->get('usercp_publicinfo_field_language_hint') . '</small>'; ?></td>
+            <td class="row1"><?php echo $lang_box; ?></td>
+          </tr>
+          <tr>
             <td class="row2"><?php echo $lang->get('usercp_publicinfo_field_changetheme_title'); ?></td>
             <td class="row1"><?php echo $lang->get('usercp_publicinfo_field_changetheme_hint'); ?> <a href="<?php echo makeUrlNS('Special', 'ChangeStyle/' . $paths->page); ?>" onclick="ajaxChangeStyle(); return false;"><?php echo $lang->get('usercp_publicinfo_field_changetheme'); ?></a></td>
           </tr>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/admin/LangManager.php	Thu Jan 24 22:06:09 2008 -0500
@@ -0,0 +1,131 @@
+<?php
+
+/*
+ * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
+ * Version 1.0.3 (Dyrad)
+ * Copyright (C) 2006-2007 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.
+ */
+
+function page_Admin_LangManager()
+{
+  global $db, $session, $paths, $template, $plugins; // Common objects
+  global $lang;
+  if ( $session->auth_level < USER_LEVEL_ADMIN || $session->user_level < USER_LEVEL_ADMIN )
+  {
+    $login_link = makeUrlNS('Special', 'Login/' . $paths->nslist['Special'] . 'Administration', 'level=' . USER_LEVEL_ADMIN, true);
+    echo '<h3>' . $lang->get('adm_err_not_auth_title') . '</h3>';
+    echo '<p>' . $lang->get('adm_err_not_auth_body', array( 'login_link' => $login_link )) . '</p>';
+    return;
+  }
+  if ( isset($_POST['action']) )
+  {
+    $action =& $_POST['action'];
+    if ( strpos($action, ';') )
+    {
+      $parms = substr($action, strpos($action, ';') + 1);
+      $action = substr($action, 0, strpos($action, ';'));
+      preg_match_all('/([a-z0-9_]+)=(.+?)(;|$)/', $parms, $matches);
+      $parms = array();
+      foreach ( $matches[0] as $i => $_ )
+      {
+        $parms[$matches[1][$i]] = $matches[2][$i];
+      }
+    }
+    switch ( $action )
+    {
+      case 'edit_language':
+        break;
+      case 'install_language':
+        $lang_list = list_available_languages();
+        // Verify that we have this language's metadata
+        if ( isset($lang_list[@$parms['iso639']]) )
+        {
+          // From here it's all downhill :-)
+          $lang_code =& $parms['iso639'];
+          $lang_data =& $lang_list[$lang_code];
+          
+          $result = install_language($lang_code, $lang_data['name_eng'], $lang_data['name']);
+          if ( $result )
+          {
+            // Language installed. Import the language files.
+            $lang_local = new Language($lang_code);
+            foreach ( array('core', 'admin', 'tools', 'user') as $file )
+            {
+              $lang_local->import(ENANO_ROOT . "/language/{$lang_data['dir']}/$file.json");
+            }
+            unset($lang_local);
+            
+            echo '<div class="info-box">' . $lang->get('acplm_msg_lang_install_success', array('lang_name' => htmlspecialchars($lang_data['name_eng']))) . '</div>';
+          }
+        }
+        break;
+    }
+  }
+  
+  // $lang_list is fetched by the posthandler sometimes
+  if ( !isset($lang_list) )
+  {
+    // Build a list of languages in the languages/ directory, then
+    // eliminate the ones that are already installed.
+    $lang_list = list_available_languages();
+  }
+  
+  // Select current languages
+  $q = $db->sql_query('SELECT lang_code FROM ' . table_prefix . "language;");
+  if ( !$q )
+    $db->_die();
+  
+  while ( $row = $db->fetchrow() )
+  {
+    $lang_code =& $row['lang_code'];
+    if ( isset($lang_list[$lang_code]) )
+    {
+      unset($lang_list[$lang_code]);
+      unset($lang_list[$lang_code]); // PHP <5.1.4 Zend bug
+    }
+  }
+  
+  if ( count($lang_list) > 0 )
+  {
+    echo '<form action="'.makeUrl($paths->nslist['Special'].'Administration', 'module='.$paths->cpage['module']).'" method="post">';
+    echo '<h3>' . $lang->get('acplm_heading_install') . '</h3>';
+    echo '<div class="tblholder">
+            <table border="0" cellspacing="1" cellpadding="4">
+              <tr>
+                <th>' . $lang->get('acplm_col_lang_code') . '</th>
+                <th>' . $lang->get('acplm_col_lang_name') . '</th>
+                <th>' . $lang->get('acplm_col_lang_name_eng') . '</th>
+                <th></th>
+              </tr>';
+              
+    $cls = 'row2';
+    foreach ( $lang_list as $lang_code => $lang_data )
+    {
+      $cls = ( $cls == 'row1' ) ? 'row2' : 'row1';
+      
+      echo '<tr>';
+      
+      $lang_code = htmlspecialchars($lang_code);
+      $lang_data['name'] = htmlspecialchars($lang_data['name']);
+      $lang_data['name_eng'] = htmlspecialchars($lang_data['name_eng']);
+      
+      echo "<td class=\"$cls\" style=\"text-align: center;\">$lang_code</td>";
+      echo "<td class=\"$cls\" style=\"text-align: center;\">{$lang_data['name']}</td>";
+      echo "<td class=\"$cls\" style=\"text-align: center;\">{$lang_data['name_eng']}</td>";
+      echo "<td class=\"$cls\" style=\"text-align: center;\"><button name=\"action\" value=\"install_language;iso639=$lang_code\">" . $lang->get('acplm_btn_install_language') . "</button></td>";
+      
+      echo '</tr>';
+    }
+    echo '    </tr>
+            </table>
+          </div>';
+    echo '</form>';        
+  }
+}
+