includes/pageprocess.php
changeset 1227 bdac73ed481e
parent 1216 4125e19d3b27
child 1252 e34c23a35dc9
--- a/includes/pageprocess.php	Sun Mar 28 21:49:26 2010 -0400
+++ b/includes/pageprocess.php	Sun Mar 28 23:10:46 2010 -0400
@@ -23,1271 +23,1271 @@
 
 class PageProcessor
 {
-  
-  /**
-   * Page ID and namespace of the page handled by this instance
-   * @var string
-   */
-  
-  var $page_id;
-  var $namespace;
-  
-  /**
-   * The instance of the namespace processor for the namespace we're doing.
-   * @var object
-   */
-  
-  var $ns;
-  
-  /**
-   * The title of the page sent to the template parser
-   * @var string
-   */
-  
-  var $title = '';
-  
-  /**
-   * The information about the page(s) we were redirected from
-   * @var array
-   */
-  
-  var $redirect_stack = array();
-  
-  /**
-   * The revision ID (history entry) to send. If set to 0 (the default) then the most recent revision will be sent.
-   * @var int
-   */
-  
-  var $revision_id = 0;
-  
-  /**
-   * The time this revision was saved, as a UNIX timestamp
-   * @var int
-   */
-  
-  var $revision_time = 0;
-  
-  /**
-   * Unsanitized page ID.
-   * @var string
-   */
-  
-  var $page_id_unclean;
-  
-  /**
-   * Tracks if the page we're loading exists in the database or not.
-   * @var bool
-   */
-  
-  var $page_exists = false;
-  
-  /**
-   * Permissions!
-   * @var object
-   */
-  
-  var $perms = null;
-  
-  /**
-   * The SHA1 hash of the user-inputted password for the page
-   * @var string
-   */
-   
-  var $password = '';
-  
-  /**
-   * Switch to track if redirects are allowed. Defaults to true.
-   * @var bool
-   */
-  
-  var $allow_redir = true;
-  
-  /**
-   * Holds any error message from redirection code. Defaults to false (no error).
-   * @var mixed
-   */
-   
-  var $redir_error = false;
-  
-  /**
-   * If this is set to true, this will call the header and footer funcs on $template when render() is called.
-   * @var bool
-   */
-  
-  var $send_headers = false;
-  
-  /**
-   * Cache the fetched text so we don't fetch it from the DB twice.
-   * @var string
-   */
-  
-  var $text_cache = '';
-  
-  /**
-   * Debugging information to track errors. You can set enable to false to disable sending debug information.
-   * @var array
-   */
-  
-  var $debug = array(
-      'enable' => false,
-      'works'  => false
-    );
-  
-  /**
-   * The list of errors raised in the class.
-   * @var array
-   */
-  
-  var $_errors = array();
-  
-  /**
-   * Constructor.
-   * @param string The page ID (urlname) of the page
-   * @param string The namespace of the page
-   * @param int Optional. The revision ID to send.
-   */
-  
-  function __construct( $page_id, $namespace, $revision_id = 0 )
-  {
-    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'] )
-    {
-      $this->debug['works'] = true;
-      $this->debug['backtrace'] = enano_debug_print_backtrace(true);
-    }
-    
-    // First things first - check page existence and permissions
-    
-    if ( !isset($paths->nslist[$namespace]) )
-    {
-      $this->send_error('The namespace "' . htmlspecialchars($namespace) . '" does not exist.');
-    }
-    
-    if ( !is_int($revision_id) )
-      $revision_id = 0;
-    
-    $this->_setup( $page_id, $namespace, $revision_id );
-  }
-  
-  /**
-   * The main method to send the page content. Also responsible for checking permissions and calling the statistics counter.
-   * @param bool If true, the stat counter is called. Defaults to false.
-   */
-  
-  function send( $do_stats = false )
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    global $lang, $output;
-    
-    profiler_log('PageProcessor: send() called');
-    
-    if ( !$this->perms->get_permissions('read') )
-    {
-      // Permission denied to read page. Is this one of our core pages that must always be allowed?
-      // NOTE: Not even the administration panel will work if ACLs deny access to it.
-      if ( $this->namespace == 'Special' && in_array($this->page_id, array('Login', 'Logout', 'LangExportJSON', 'CSS')) )
-      {
-        // Do nothing; allow execution to continue
-      }
-      else
-      {
-        // Page isn't whitelisted, behave as normal
-        $this->err_access_denied();
-        return false;
-      }
-    }
-    if ( $this->revision_id > 0 && !$this->perms->get_permissions('history_view') )
-    {
-      $this->err_access_denied();
-      return false;
-    }
-    
-    // Is there a custom function registered for handling this namespace?
-    // DEPRECATED (even though it only saw its way into one alpha release.)
-    if ( $proc = $paths->get_namespace_processor($this->namespace) )
-    {
-      // yes, just call that
-      // this is protected aggressively by the PathManager against overriding critical namespaces
-      return call_user_func($proc, $this);
-    }
-    
-    $pathskey = $paths->nslist[ $this->namespace ] . $this->page_id;
-    $strict_no_headers = false;
-    $admin_fail = false;
-    if ( $this->namespace == 'Admin' && strstr($this->page_id, '/') )
-    {
-      $this->page_id = substr($this->page_id, 0, strpos($this->page_id, '/'));
-      $funcname = "page_{$this->namespace}_{$this->page_id}";
-      if ( function_exists($funcname) )
-      {
-        $this->page_exists = true;
-      }
-    }
-    if ( isPage($pathskey) )
-    {
-      $cdata = $this->ns->get_cdata();
-      
-      if ( $cdata['special'] == 1 )
-      {
-        $this->send_headers = false;
-        $strict_no_headers = true;
-        $GLOBALS['output'] = new Output_Naked();
-      }
-      if ( isset($cdata['password']) )
-      {
-        if ( $cdata['password'] != '' && $cdata['password'] != sha1('') )
-        {
-          $password =& $cdata['password'];
-          if ( $this->password != $password )
-          {
-            $this->err_wrong_password();
-            return false;
-          }
-        }
-      }
-      if ( isset($cdata['require_admin']) && $cdata['require_admin'] )
-      {
-        if ( $session->auth_level < USER_LEVEL_ADMIN )
-        {
-          $admin_fail = true;
-        }
-      }
-    }
-    else if ( $this->namespace === $paths->namespace && $this->page_id == $paths->page_id )
-    {
-      if ( isset($paths->cpage['require_admin']) && $paths->cpage['require_admin'] )
-      {
-        if ( $session->auth_level < USER_LEVEL_ADMIN )
-        {
-          $admin_fail = true;
-        }
-      }
-    }
-    if ( $admin_fail )
-    {
-      header('Content-type: text/javascript');
-      echo enano_json_encode(array(
-          'mode' => 'error',
-          'error' => 'need_auth_to_admin'
-        ));
-      return true;
-    }
-    if ( $this->page_exists && $this->namespace != 'Special' && $this->namespace != 'Admin' && $do_stats )
-    {
-      require_once(ENANO_ROOT.'/includes/stats.php');
-      doStats($this->page_id, $this->namespace);
-    }
-    
-    // We are all done. Ship off the page.
-    
-    if ( !$this->allow_redir )
-    {
-      if ( method_exists($this->ns, 'get_redirect') )
-      {
-        if ( $result = $this->ns->get_redirect() )
-          display_redirect_notice($result['page_id'], $result['namespace']);
-      }
-    }
-    else
-    {
-      $this->process_redirects();
-      
-      if ( count($this->redirect_stack) > 0 )
-      {
-        $stack = array_reverse($this->redirect_stack);
-        foreach ( $stack as $stackel )
-        {
-          $url = makeUrlNS($stackel['old_namespace'], $stackel['old_page_id'], 'redirect=no', true);
-          $page_data = $this->ns->get_cdata();
-          $title = $stackel['old_title'];
-          $a = '<a href="' . $url . '">' . htmlspecialchars($title) . '</a>';
-          $output->add_after_header('<small>' . $lang->get('page_msg_redirected_from', array('from' => $a)) . '<br /></small>');
-        }
-        $template->set_page($this);
-      }
-      
-      if ( $this->redir_error )
-      {
-        $output->add_after_header('<div class="usermessage"><b>' . $this->redir_error . '</b></div>');
-        $result = $this->ns->get_redirect();
-        display_redirect_notice($result['page_id'], $result['namespace']);
-      }
-    }
-    
-    $this->ns->send();
-  }
-  
-  /**
-   * Sends the page through by fetching it from the database.
-   */
-   
-  function send_from_db($strict_no_headers = false)
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    global $lang;
-    
-    $this->ns->send_from_db();
-  }
-  
-  /**
-   * Fetches the wikitext or HTML source for the page.
-   * @return string
-   */
-  
-  function fetch_source()
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    
-    if ( !$this->perms->get_permissions('view_source') )
-    {
-      return false;
-    }
-    if ( !$this->page_exists )
-    {
-      return '';
-    }
-    $cdata = $this->ns->get_cdata();
-    if ( isset($cdata['password']) )
-    {
-      if ( $cdata['password'] != sha1('') && $cdata['password'] !== $this->password && !empty($cdata['password']) )
-      {
-        return false;
-      }
-    }
-    return $this->fetch_text();
-  }
-  
-  /**
-   * Updates (saves/changes/edits) the content of the page.
-   * @param string The new text for the page
-   * @param string A summary of edits made to the page.
-   * @param bool If true, the edit is marked as a minor revision
-   * @param string Page format - wikitext or xhtml. REQUIRED, and new in 1.1.6.
-   * @return bool True on success, false on failure. When returning false, it will push errors to the PageProcessor error stack; read with $page->pop_error()
-   */
-  
-  function update_page($text, $edit_summary = false, $minor_edit = false, $page_format)
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    global $lang;
-    
-    // Create the page if it doesn't exist
-    if ( !$this->page_exists )
-    {
-      if ( !$this->create_page() )
-      {
-        return false;
-      }
-    }
-      
-    //
-    // Validation
-    //
-    
-    $page_id = $db->escape($this->page_id);
-    $namespace = $db->escape($this->namespace);
-    
-    $q = $db->sql_query('SELECT protected FROM ' . table_prefix . "pages WHERE urlname='$page_id' AND namespace='$namespace';");
-    if ( !$q )
-      $db->_die('PageProcess updating page content');
-    if ( $db->numrows() < 1 )
-    {
-      $this->raise_error($lang->get('editor_err_no_rows'));
-      return false;
-    }
-    
-    // Do we have permission to edit the page?
-    if ( !$this->perms->get_permissions('edit_page') )
-    {
-      $this->raise_error($lang->get('editor_err_no_permission'));
-      return false;
-    }
-    
-    list($protection) = $db->fetchrow_num();
-    $db->free_result();
-    
-    if ( $protection == 1 )
-    {
-      // The page is protected - do we have permission to edit protected pages?
-      if ( !$this->perms->get_permissions('even_when_protected') )
-      {
-        $this->raise_error($lang->get('editor_err_page_protected'));
-        return false;
-      }
-    }
-    else if ( $protection == 2 )
-    {
-      // The page is semi-protected.
-      if (
-           ( !$session->user_logged_in || // Is the user logged in?
-             ( $session->user_logged_in && $session->reg_time + ( 4 * 86400 ) >= time() ) ) // If so, have they been registered for 4 days?
-           && !$this->perms->get_permissions('even_when_protected') ) // And of course, is there an ACL that overrides semi-protection?
-      {
-        $this->raise_error($lang->get('editor_err_page_protected'));
-        return false;
-      }
-    }
-    
-    // Spam check
-    if ( !spamalyze($text) )
-    {
-      $this->raise_error($lang->get('editor_err_spamcheck_failed'));
-      return false;
-    }
-    
-    // Page format check
-    if ( !in_array($page_format, array('xhtml', 'wikitext')) )
-    {
-      $this->raise_error("format \"$page_format\" not one of [xhtml, wikitext]");
-      return false;
-    }
-    
-    //
-    // Protection validated; update page content
-    //
-    
-    $text_undb = RenderMan::preprocess_text($text, false, false);
-    $text = $db->escape($text_undb);
-    $author = $db->escape($session->username);
-    $time = time();
-    $edit_summary = ( strval($edit_summary) === $edit_summary ) ? $db->escape($edit_summary) : '';
-    $minor_edit = ( $minor_edit ) ? '1' : '0';
-    $date_string = enano_date(ED_DATE | ED_TIME);
-    
-    // Insert log entry
-    $sql = 'INSERT INTO ' . table_prefix . "logs ( time_id, date_string, log_type, action, page_id, namespace, author, author_uid, page_text, edit_summary, minor_edit, page_format )\n"
-         . "  VALUES ( $time, '$date_string', 'page', 'edit', '{$this->page_id}', '{$this->namespace}', '$author', $session->user_id, '$text', '$edit_summary', $minor_edit, '$page_format' );";
-    if ( !$db->sql_query($sql) )
-    {
-      $this->raise_error($db->get_error());
-      return false;
-    }
-    
-    // Update the master text entry
-    $sql = 'UPDATE ' . table_prefix . "page_text SET page_text = '$text' WHERE page_id = '{$this->page_id}' AND namespace = '{$this->namespace}';";
-    if ( !$db->sql_query($sql) )
-    {
-      $this->raise_error($db->get_error());
-      return false;
-    }
-    
-    // If there's an identical draft copy, delete it
-    $sql = 'DELETE FROM ' . table_prefix . "logs WHERE is_draft = 1 AND page_id = '{$this->page_id}' AND namespace = '{$this->namespace}' AND page_text = '{$text}';";
-    if ( !$db->sql_query($sql) )
-    {
-      $this->raise_error($db->get_error());
-      return false;
-    }
-    
-    // Set page_format
-    // Using @ due to warning thrown when saving new page
-    $cdata = $this->ns->get_cdata();
-    if ( @$cdata['page_format'] !== $page_format )
-    {
-      // Note: no SQL injection to worry about here. Everything that goes into this is sanitized already, barring some rogue plugin.
-      // (and if there's a rogue plugin running, we have bigger things to worry about anyway.)
-      if ( !$db->sql_query('UPDATE ' . table_prefix . "pages SET page_format = '$page_format' WHERE urlname = '$this->page_id' AND namespace = '$this->namespace';") )
-      {
-        $this->raise_error($db->get_error());
-        return false;
-      }
-      $paths->update_metadata_cache();
-    }
-    
-    // Rebuild the search index
-    $paths->rebuild_page_index($this->page_id, $this->namespace);
-    
-    $this->text_cache = $text_undb;
-    
-    return true;
-    
-  }
-  
-  /**
-   * Creates the page if it doesn't already exist.
-   * @param string Optional page title.
-   * @param bool Visibility (allow indexing) flag
-   * @return bool True on success, false on failure.
-   */
-  
-  function create_page($title = false, $visible = true)
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    global $lang;
-    
-    // Do we have permission to create the page?
-    if ( !$this->perms->get_permissions('create_page') )
-    {
-      $this->raise_error($lang->get('pagetools_create_err_no_permission'));
-      return false;
-    }
-    
-    // Does it already exist?
-    if ( $this->page_exists )
-    {
-      $this->raise_error($lang->get('pagetools_create_err_already_exists'));
-      return false;
-    }
-    
-    // It's not in there. Perform validation.
-    
-    // We can't create special, admin, or external pages.
-    if ( $this->namespace == 'Special' || $this->namespace == 'Admin' || $this->namespace == 'API' )
-    {
-      $this->raise_error($lang->get('pagetools_create_err_nodb_namespace'));
-      return false;
-    }
-    
-    // Guess the proper title
-    $name = ( !empty($title) ) ? $title : str_replace('_', ' ', dirtify_page_id($this->page_id));
-    
-    // Check for the restricted Project: prefix
-    if ( substr($this->page_id, 0, 8) == 'Project:' )
-    {
-      $this->raise_error($lang->get('pagetools_create_err_reserved_prefix'));
-      return false;
-    }
-    
-    // Validation successful - insert the page
-    
-    $metadata = array(
-        'urlname' => $this->page_id,
-        'namespace' => $this->namespace,
-        'name' => $name,
-        'special' => 0,
-        'visible' => $visible ? 1 : 0,
-        'comments_on' => 1,
-        'protected' => ( $this->namespace == 'System' ? 1 : 0 ),
-        'delvotes' => 0,
-        'delvote_ips' => serialize(array()),
-        'wiki_mode' => 2
-      );
-    
-    $paths->add_page($metadata);
-    
-    $page_id = $db->escape($this->page_id);
-    $namespace = $db->escape($this->namespace);
-    $name = $db->escape($name);
-    $protect = ( $this->namespace == 'System' ) ? '1' : '0';
-    $blank_array = $db->escape(serialize(array()));
-    
-    // Query 1: Metadata entry
-    $q = $db->sql_query('INSERT INTO ' . table_prefix . "pages(name, urlname, namespace, visible, protected, delvotes, delvote_ips, wiki_mode)\n"
-                      . "  VALUES ( '$name', '$page_id', '$namespace', {$metadata['visible']}, $protect, 0, '$blank_array', 2 );");
-    if ( !$q )
-      $db->_die('PageProcessor page creation - metadata stage');
-    
-    // Query 2: Text insertion
-    $q = $db->sql_query('INSERT INTO ' . table_prefix . "page_text(page_id, namespace, page_text)\n"
-                        . "VALUES ( '$page_id', '$namespace', '' );");
-    if ( !$q )
-      $db->_die('PageProcessor page creation - text stage');
-    
-    // Query 3: Log entry
-    $db->sql_query('INSERT INTO ' . table_prefix."logs(time_id, date_string, log_type, action, author, author_uid, page_id, namespace)\n"
-                   . "  VALUES ( " . time() . ", 'DEPRECATED', 'page', 'create', \n"
-                   . "          '" . $db->escape($session->username) . "', $session->user_id, '" . $db->escape($this->page_id) . "', '" . $this->namespace . "');");
-    if ( !$q )
-      $db->_die('PageProcessor page creation - logging stage');
-    
-    // Update the cache
-    $paths->update_metadata_cache();
-    
-    // Make sure that when/if we save the page later in this instance it doesn't get re-created
-    $this->page_exists = true;
-    
-    // Page created. We're good!
-    return true;
-  }
-  
-  /**
-   * Rolls back a non-edit action in the logs
-   * @param int Log entry (log_id) to roll back
-   * @return array Standard Enano error/success protocol
-   */
-  
-  function rollback_log_entry($log_id)
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    global $cache;
-    
-    // Verify permissions
-    if ( !$this->perms->get_permissions('history_rollback') )
-    {
-      return array(
-        'success' => false,
-        'error' => 'access_denied'
-        );
-    }
-    
-    // Check input
-    $log_id = intval($log_id);
-    if ( empty($log_id) )
-    {
-      return array(
-        'success' => false,
-        'error' => 'invalid_parameter'
-        );
-    }
-    
-    // Fetch the log entry
-    $q = $db->sql_query('SELECT * FROM ' . table_prefix . "logs WHERE log_type = 'page' AND page_id='{$this->page_id}' AND namespace='{$this->namespace}' AND log_id = $log_id;");
-    if ( !$q )
-      $db->_die();
-    
-    // Is this even a valid log entry for this context?
-    if ( $db->numrows() < 1 )
-    {
-      return array(
-        'success' => false,
-        'error' => 'entry_not_found'
-        );
-    }
-    
-    // All good, fetch and free the result
-    $log_entry = $db->fetchrow();
-    $db->free_result();
-    
-    $dateline = enano_date(ED_DATE | ED_TIME, $log_entry['time_id']);
-    
-    // Let's see, what do we have here...
-    switch ( $log_entry['action'] )
-    {
-      case 'rename':
-        // Page was renamed, let the rename method handle this
-        return array_merge($this->rename_page($log_entry['edit_summary']), array('dateline' => $dateline, 'action' => $log_entry['action']));
-        break;
-      case 'prot':
-      case 'unprot':
-      case 'semiprot':
-        return array_merge($this->protect_page(intval($log_entry['page_text']), '__REVERSION__'), array('dateline' => $dateline, 'action' => $log_entry['action']));
-        break;
-      case 'delete':
-        
-        // Raising a previously dead page has implications...
-        
-        // FIXME: l10n
-        // rollback_extra is required because usually only moderators can undo page deletion AND restore the content.
-        // potential flaw here - once recreated, can past revisions be restored by users without rollback_extra? should
-        // probably modify editor routine to deny revert access if the timestamp < timestamp of last deletion if any.
-        if ( !$this->perms->get_permissions('history_rollback_extra') )
-          return 'Administrative privileges are required for page undeletion.';
-        
-        // Rolling back the deletion of a page that was since created?
-        $pathskey = $paths->nslist[ $this->namespace ] . $this->page_id;
-        if ( isPage($pathskey) )
-          return array(
-              'success' => false,
-              // This is a clean Christian in-joke.
-              'error' => 'seeking_living_among_dead'
-            );
-        
-        // Generate a crappy page name
-        $name = $db->escape( str_replace('_', ' ', dirtify_page_id($this->page_id)) );
-        
-        // Stage 1 - re-insert page
-        $e = $db->sql_query('INSERT INTO ' . table_prefix.'pages(name,urlname,namespace) VALUES( \'' . $name . '\', \'' . $this->page_id . '\',\'' . $this->namespace . '\' )');
-        if ( !$e )
-          $db->die_json();
-        
-        // Select the latest published revision
-        $q = $db->sql_query('SELECT page_text FROM ' . table_prefix . "logs WHERE\n"
-                          . "      log_type  = 'page'\n"
-                          . "  AND action    = 'edit'\n"
-                          . "  AND page_id   = '$this->page_id'\n"
-                          . "  AND namespace = '$this->namespace'\n"
-                          . "  AND is_draft != 1\n"
-                          . "ORDER BY time_id DESC LIMIT 1;");
-        if ( !$q )
-          $db->die_json();
-        list($page_text) = $db->fetchrow_num();
-        $db->free_result($q);
-        
-        // Apply the latest revision as the current page text
-        $page_text = $db->escape($page_text);
-        $e = $db->sql_query('INSERT INTO ' . table_prefix."page_text(page_id, namespace, page_text) VALUES\n"
-                          . "  ( '$this->page_id', '$this->namespace', '$page_text' );");
-        if ( !$e )
-          $db->die_json();
-        
-        $cache->purge('page_meta');
-        
-        return array(
-            'success' => true,
-            'dateline' => $dateline,
-            'action' => $log_entry['action']
-          );
-        
-        break;
-      case 'reupload':
-        
-        // given a log id and some revision info, restore the old file.
-        // get the timestamp of the file before this one
-        $q = $db->sql_query('SELECT time_id, file_key, file_extension, filename, size, mimetype FROM ' . table_prefix . "files WHERE time_id < {$log_entry['time_id']} ORDER BY time_id DESC LIMIT 1;");
-        if ( !$q )
-          $db->_die();
-        
-        $row = $db->fetchrow();
-        $db->free_result();
-        
-        // If the file hasn't been renamed to the new format (omitting timestamp), do that now.
-        $fname = ENANO_ROOT . "/files/{$row['file_key']}_{$row['time_id']}{$row['file_extension']}";
-        if ( @file_exists($fname) )
-        {
-          // it's stored in the old format - rename
-          $fname_new = ENANO_ROOT . "/files/{$row['file_key']}{$row['file_extension']}";
-          if ( !@rename($fname, $fname_new) )
-          {
-            return array(
-              'success' => false,
-              'error' => 'rb_file_rename_failed',
-              'action' => $log_entry['action']
-              );
-          }
-        }
-        
-        // Insert a new file entry
-        $time = time();
-        $filename = $db->escape($row['filename']);
-        $mimetype = $db->escape($row['mimetype']);
-        $ext = $db->escape($row['file_extension']);
-        $key = $db->escape($row['file_key']);
-        
-        $q = $db->sql_query('INSERT INTO ' . table_prefix . "files ( time_id, page_id, filename, size, mimetype, file_extension, file_key ) VALUES\n"
-              . "  ( $time, '$this->page_id', '$filename', {$row['size']}, '$mimetype', '$ext', '$key' );");
-        if ( !$q )
-          $db->die_json();
-        
-        // add reupload log entry
-        $username = $db->escape($session->username);
-        $q = $db->sql_query('INSERT INTO ' . table_prefix . "logs ( log_type, action, time_id, page_id, namespace, author, author_uid, edit_summary ) VALUES\n"
-                          . "  ( 'page', 'reupload', $time, '$this->page_id', '$this->namespace', '$username', $session->user_id, '__ROLLBACK__' )");
-        if ( !$q )
-          $db->die_json();
-        
-        return array(
-            'success' => true,
-            'dateline' => $dateline,
-            'action' => $log_entry['action']
-          );
-        
-        break;
-      case 'votereset':
-        if ( !$this->perms->get_permissions('history_rollback_extra') )
-          return 'Denied!';
-        
-        // pull existing vote data
-        $q = $db->sql_query('SELECT delvotes, delvote_ips FROM ' . table_prefix . "pages WHERE urlname = '$this->page_id' AND namespace = '$this->namespace';");
-        if ( !$q )
-          $db->_die();
-        
-        if ( $db->numrows() < 1 )
-          return array(
-              'success' => false,
-              'error' => 'page_not_exist',
-              'action' => $log_entry['action']
-            );
-          
-        list($curr_delvotes, $curr_delvote_ips) = $db->fetchrow_num();
-        $db->free_result();
-        
-        // merge with existing votes
-        $old_delvote_ips = unserialize($log_entry['page_text']);
-        $new_delvote_ips = unserialize($curr_delvote_ips);
-        $new_delvote_ips['u'] = array_unique(array_merge($new_delvote_ips['u'], $old_delvote_ips['u']));
-        $new_delvote_ips['ip'] = array_unique(array_merge($new_delvote_ips['ip'], $old_delvote_ips['ip']));
-        $new_delvotes = count($new_delvote_ips['ip']);
-        $new_delvote_ips = $db->escape(serialize($new_delvote_ips));
-        
-        // update pages table
-        $q = $db->sql_query('UPDATE ' . table_prefix . "pages SET delvotes = $new_delvotes, delvote_ips = '$new_delvote_ips' WHERE urlname = '$this->page_id' AND namespace = '$this->namespace';");
-        
-        $cache->purge('page_meta');
-        
-        return array(
-            'success' => true,
-            'dateline' => $dateline,
-            'action' => $log_entry['action']
-          );
-        break;
-      default:
-        
-        return array(
-            'success' => false,
-            'error' => 'rb_action_not_supported',
-            'action' => $log_entry['action']
-          );
-        
-        break;
-    }
-  }
-  
-  /**
-   * Renames the page
-   * @param string New name
-   * @return array Standard Enano error/success protocol
-   */
-  
-  function rename_page($new_name)
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    
-    // Check permissions
-    if ( !$this->perms->get_permissions('rename') )
-    {
-      return array(
-        'success' => false,
-        'error' => 'access_denied'
-        );
-    }
-    
-    // If this is the same as the current name, return success
-    $page_name = get_page_title_ns($this->page_id, $this->namespace);
-    if ( $page_name === $new_name )
-    {
-      return array(
-        'success' => true
-        );
-    }
-    
-    // Make sure the name is valid
-    $new_name = trim($new_name);
-    if ( empty($new_name) )
-    {
-      return array(
-        'success' => false,
-        'error' => 'invalid_parameter'
-        );
-    }
-    
-    // Log the action
-    $username = $db->escape($session->username);
-    $page_name = $db->escape($page_name);
-    $time = time();
-    
-    $q = $db->sql_query('INSERT INTO ' . table_prefix . "logs ( log_type, action, page_id, namespace, author, author_uid, edit_summary, time_id, date_string ) VALUES\n"
-                      . "  ( 'page', 'rename', '{$this->page_id}', '{$this->namespace}', '$username', $session->user_id, '$page_name', '$time', 'DATE_STRING COLUMN OBSOLETE, USE time_id' );");
-    if ( !$q )
-      $db->_die();
-    
-    // Not much to do but to rename it now
-    $new_name = $db->escape($new_name);
-    $q = $db->sql_query('UPDATE ' . table_prefix . "pages SET name = '$new_name' WHERE urlname = '{$this->page_id}' AND namespace = '{$this->namespace}';");
-    if ( !$q )
-      $db->_die();
-    
-    // Update the cache
-    $paths->update_metadata_cache();
-    
-    return array(
-      'success' => true
-      );
-  }
-  
-  /**
-   * Sets the protection level of the page
-   * @param int Protection level, one of PROTECT_{FULL,SEMI,NONE}
-   * @param string Reason for protection - required
-   */
-  
-  function protect_page($protection_level, $reason)
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    global $cache;
-    
-    // Validate permissions
-    if ( !$this->perms->get_permissions('protect') )
-    {
-      return array(
-        'success' => false,
-        'error' => 'access_denied'
-        );
-    }
-    
-    // Validate re-auth
-    if ( !$session->sid_super )
-    {
-      return array(
-        'success' => false,
-        'error' => 'access_denied_need_reauth'
-        );
-    }
-    
-    // Validate input
-    $reason = trim($reason);
-    if ( !in_array($protection_level, array(PROTECT_NONE, PROTECT_FULL, PROTECT_SEMI)) || empty($reason) )
-    {
-      return array(
-        'success' => false,
-        'error' => 'invalid_parameter'
-        );
-    }
-    
-    // Retrieve page metadata
-    $metadata = $this->ns->get_cdata();
-    
-    // Log the action
-    $username = $db->escape($session->username);
-    $time = time();
-    $existing_protection = intval($metadata['protected']);
-    $reason = $db->escape($reason);
-    
-    if ( $existing_protection == $protection_level )
-    {
-      return array(
-        'success' => false,
-        'error' => 'protection_already_there'
-        );
-    }
-    
-    $action = '[ insanity ]';
-    switch($protection_level)
-    {
-      case PROTECT_FULL: $action = 'prot'; break;
-      case PROTECT_NONE: $action = 'unprot'; break;
-      case PROTECT_SEMI: $action = 'semiprot'; break;
-    }
-    
-    $sql = 'INSERT INTO ' . table_prefix . "logs ( log_type, action, page_id, namespace, author, author_uid, edit_summary, time_id, page_text, date_string ) VALUES\n"
-         . "  ( 'page', '$action', '{$this->page_id}', '{$this->namespace}', '$username', $author_uid, '$reason', '$time', '$existing_protection', 'DATE_STRING COLUMN OBSOLETE, USE time_id' );";
-    if ( !$db->sql_query($sql) )
-    {
-      $db->die_json();
-    }
-    
-    // Perform the actual protection
-    $q = $db->sql_query('UPDATE ' . table_prefix . "pages SET protected = $protection_level WHERE urlname = '{$this->page_id}' AND namespace = '{$this->namespace}';");
-    if ( !$q )
-      $db->die_json();
-    
-    $cache->purge('page_meta');
-    
-    return array(
-      'success' => true
-      );
-  }
-  
-  /**
-   * Sets internal variables.
-   * @access private
-   */
-  
-  function _setup($page_id, $namespace, $revision_id)
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    
-    $page_id_cleaned = sanitize_page_id($page_id);
-    
-    $this->revision_id = $revision_id;
-    $this->page_id_unclean = dirtify_page_id($page_id);
-    
-    // resolve namespace
-    $this->ns = namespace_factory($page_id, $namespace, $this->revision_id);
-    $this->page_id =& $this->ns->page_id;
-    $this->namespace =& $this->ns->namespace;
-    
-    $this->perms = $session->fetch_page_acl( $page_id, $namespace );
-    
-    $this->page_exists = $this->ns->exists();
-    $this->title = get_page_title_ns($this->page_id, $this->namespace);
-    
-    profiler_log("PageProcessor [{$this->namespace}:{$this->page_id}]: Ran _setup()");
-  }
-  
-  /**
-   * Processes any redirects.
-   * @access private
-   */
-  
-  function process_redirects()
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    global $output, $lang;
-    
-    $this->redirect_stack = array();
-    
-    if ( !method_exists($this->ns, 'get_redirect') )
-      return true;
-    
-    if ( !$this->allow_redir )
-      return true;
-    
-    $redirect_count = 0;
-    
-    while ( $result = $this->ns->get_redirect() )
-    {
-      if ( $result['namespace'] == 'Special' || $result['namespace'] == 'Admin' )
-      {
-        // Can't redirect to special/admin page
-        $this->redir_error = $lang->get('page_err_redirect_to_special');
-        break;
-      }
-      if ( $redirect_count == 3 )
-      {
-        // max of 3 internal redirects exceeded
-        $this->redir_error = $lang->get('page_err_redirects_exceeded');
-        break;
-      }
-      
-      $loop = false;
-      foreach ( $this->redirect_stack as $stackel )
-      {
-        if ( $result['page_id'] == $stackel['old_page_id'] && $result['namespace'] == $stackel['old_namespace'] )
-        {
-          $loop = true;
-          break;
-        }
-      }
-      
-      if ( $loop )
-      {
-        // redirect loop
-        $this->redir_error = $lang->get('page_err_redirect_infinite_loop');
-        break;
-      }
-      $new_ns = namespace_factory($result['page_id'], $result['namespace']);
-      if ( !$new_ns->exists() )
-      {
-        // new page doesn't exist
-        $this->redir_error = $lang->get('page_err_redirect_to_nonexistent');
-        break;
-      }
-      
-      // build stack entry
-      $stackel = array(
-          'page_id' => $result['page_id'],
-          'namespace' => $result['namespace'],
-          'old_page_id' => $this->page_id,
-          'old_namespace' => $this->namespace,
-          'old_title' => $this->ns->title
-        );
-      
-      // replace everything (perform the actual redirect)
-      $this->ns = $new_ns;
-      
-      $this->page_id =& $this->ns->page_id;
-      $this->namespace =& $this->ns->namespace;
-      
-      $this->redirect_stack[] = $stackel;
-      
-      $redirect_count++;
-    }
-  }
-    
-  /**
-   * Sends the page header, dependent on, of course, whether we're supposed to.
-   */
-  
-  function header()
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    if ( $this->send_headers )
-      $template->header();
-  }
-  
-  /**
-   * Sends the page footer, dependent on, of course, whether we're supposed to.
-   */
-  
-  function footer()
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    if ( $this->send_headers )
-      $template->footer();
-  }
-  
-  /**
-   * Fetches the raw, unfiltered page text.
-   * @access public
-   */
-  
-  function fetch_text()
-  {
-    return $this->ns->fetch_text();
-  }
-  
-  /**
-   * Tells us if the page exists.
-   * @return bool
-   */
-  
-  function exists()
-  {
-    return $this->ns->exists();
-  }
-  
-  /**
-   * Send the error message to the user that the access to this page is denied.
-   * @access private
-   */
-  
-  function err_access_denied()
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    global $lang;
-    global $email;
-    
-    // Log it for crying out loud
-    $q = $db->sql_query('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,author_uid,edit_summary,page_text) VALUES(\'security\', \'illegal_page\', '.time().', \'DEPRECATED\', \''.$db->escape($session->username).'\', ' . $session->user_id . ', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\', \'' . $db->escape(serialize(array($this->page_id, $this->namespace))) . '\')');
-    
-    $ob = '';
-    //$template->tpl_strings['PAGE_NAME'] = 'Access denied';
-    $template->tpl_strings['PAGE_NAME'] = htmlspecialchars( $this->title );
-      
-    if ( $this->send_headers )
-    {
-      $ob .= $template->getHeader();
-    }
-    
-    if ( count($this->redirect_stack) > 0 )
-    {
-      $stack = array_reverse($this->redirect_stack);
-      foreach ( $stack as $oldtarget )
-      {
-        $url = makeUrlNS($oldtarget[1], $oldtarget[0], 'redirect=no', true);
-        $old_page = namespace_factory($oldtarget[0], $oldtarget[1]);
-        $page_data = $old_page->get_cdata();
-        $title = ( isset($page_data['name']) ) ? $page_data['name'] : $paths->nslist[$oldtarget[1]] . htmlspecialchars( str_replace('_', ' ', dirtify_page_id( $oldtarget[0] ) ) );
-        $a = '<a href="' . $url . '">' . $title . '</a>';
-        
-        $url = makeUrlNS($this->namespace, $this->page_id, 'redirect=no', true);
-        $page_data = $this->ns->get_cdata();
-        $title = ( isset($page_data['name']) ) ? $page_data['name'] : $paths->nslist[$this->namespace] . htmlspecialchars( str_replace('_', ' ', dirtify_page_id( $this->page_id ) ) );
-        $b = '<a href="' . $url . '">' . $title . '</a>';
-        
-        $ob .= '<small>' . $lang->get('page_msg_redirected_from_to', array('from' => $a, 'to' => $b)) . '<br /></small>';
-      }
-    }
-    
-    $email_link = $email->encryptEmail(getConfig('contact_email'), '', '', $lang->get('page_err_access_denied_siteadmin'));
-    
-    $ob .= "<h3>" . $lang->get('page_err_access_denied_title') . "</h3>";
-    $ob .= "<p>" . $lang->get('page_err_access_denied_body', array('site_administration' => $email_link)) . "</p>";
-    
-    if ( $this->send_headers )
-    {
-      $ob .= $template->getFooter();
-    }
-    echo $ob;
-  }
-  
-  /**
-   * Inform the user of an incorrect or absent password
-   * @access private
-   */
-   
-  function err_wrong_password()
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    global $lang;
-    
-    $title = $lang->get('page_msg_passrequired_title');
-    $message = ( empty($this->password) ) ?
-                 '<p>' . $lang->get('page_msg_passrequired') . '</p>' :
-                 '<p>' . $lang->get('page_msg_pass_wrong') . '</p>';
-    $message .= '<form action="' . makeUrlNS($this->namespace, $this->page_id) . '" method="post">
-                   <p>
-                     <label>' . $lang->get('page_lbl_password') . ' <input name="pagepass" type="password" /></label>&nbsp;&nbsp;<input type="submit" value="' . $lang->get('page_btn_password_submit') . '" />
-                   </p>
-                 </form>';
-    if ( $this->send_headers )
-    {
-      $template->tpl_strings['PAGE_NAME'] = $title;
-      $template->header();
-      echo "$message";
-      $template->footer();
-    }
-    else
-    {
-      echo "<h2>$title</h2>
-            $message";
-    }
-  }
-  
-  /**
-   * Send the error message to the user complaining that there weren't any rows.
-   * @access private
-   */
-  
-  function err_no_rows()
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    
-    $title = 'No text rows';
-    $message = 'While the page\'s existence was verified, there were no rows in the database that matched the query for the text. This may indicate a bug with the software; ask the webmaster for more information. The offending query was:<pre>' . $db->latest_query . '</pre>';
-    if ( $this->send_headers )
-    {
-      $template->tpl_strings['PAGE_NAME'] = $title;
-      $template->header();
-      echo "<p>$message</p>";
-      $template->footer();
-    }
-    else
-    {
-      echo "<h2>$title</h2>
-            <p>$message</p>";
-    }
-  }
-  
-  /**
-   * Send an error message and die. For debugging or critical technical errors only - nothing that would under normal circumstances be shown to the user.
-   * @param string Error message
-   * @param bool If true, send DBAL's debugging information as well
-   */
-   
-  function send_error($message, $sql = false)
-  {
-    global $db, $session, $paths, $template, $plugins; // Common objects
-    global $lang;
-    
-    $content = "<p>$message</p>";
-    $template->tpl_strings['PAGE_NAME'] = $lang->get('page_msg_general_error');
-    
-    if ( $this->debug['works'] )
-    {
-      $content .= $this->debug['backtrace'];
-    }
-    
-    header('HTTP/1.1 500 Internal Server Error');
-    
-    $template->header();
-    echo $content;
-    $template->footer();
-    
-    $db->close();
-    
-    exit;
-    
-  }
-  
-  /**
-   * Raises an error.
-   * @param string Error string
-   */
-   
-  function raise_error($string)
-  {
-    if ( !is_string($string) )
-      return false;
-    $this->_errors[] = $string;
-  }
-  
-  /**
-   * Retrieves the latest error from the error stack and returns it ('pops' the error stack)
-   * @return string
-   */
-  
-  function pop_error()
-  {
-    if ( count($this->_errors) < 1 )
-      return false;
-    return array_pop($this->_errors);
-  }
-  
+	
+	/**
+ 	* Page ID and namespace of the page handled by this instance
+ 	* @var string
+ 	*/
+	
+	var $page_id;
+	var $namespace;
+	
+	/**
+ 	* The instance of the namespace processor for the namespace we're doing.
+ 	* @var object
+ 	*/
+	
+	var $ns;
+	
+	/**
+ 	* The title of the page sent to the template parser
+ 	* @var string
+ 	*/
+	
+	var $title = '';
+	
+	/**
+ 	* The information about the page(s) we were redirected from
+ 	* @var array
+ 	*/
+	
+	var $redirect_stack = array();
+	
+	/**
+ 	* The revision ID (history entry) to send. If set to 0 (the default) then the most recent revision will be sent.
+ 	* @var int
+ 	*/
+	
+	var $revision_id = 0;
+	
+	/**
+ 	* The time this revision was saved, as a UNIX timestamp
+ 	* @var int
+ 	*/
+	
+	var $revision_time = 0;
+	
+	/**
+ 	* Unsanitized page ID.
+ 	* @var string
+ 	*/
+	
+	var $page_id_unclean;
+	
+	/**
+ 	* Tracks if the page we're loading exists in the database or not.
+ 	* @var bool
+ 	*/
+	
+	var $page_exists = false;
+	
+	/**
+ 	* Permissions!
+ 	* @var object
+ 	*/
+	
+	var $perms = null;
+	
+	/**
+ 	* The SHA1 hash of the user-inputted password for the page
+ 	* @var string
+ 	*/
+ 	
+	var $password = '';
+	
+	/**
+ 	* Switch to track if redirects are allowed. Defaults to true.
+ 	* @var bool
+ 	*/
+	
+	var $allow_redir = true;
+	
+	/**
+ 	* Holds any error message from redirection code. Defaults to false (no error).
+ 	* @var mixed
+ 	*/
+ 	
+	var $redir_error = false;
+	
+	/**
+ 	* If this is set to true, this will call the header and footer funcs on $template when render() is called.
+ 	* @var bool
+ 	*/
+	
+	var $send_headers = false;
+	
+	/**
+ 	* Cache the fetched text so we don't fetch it from the DB twice.
+ 	* @var string
+ 	*/
+	
+	var $text_cache = '';
+	
+	/**
+ 	* Debugging information to track errors. You can set enable to false to disable sending debug information.
+ 	* @var array
+ 	*/
+	
+	var $debug = array(
+			'enable' => false,
+			'works'  => false
+		);
+	
+	/**
+ 	* The list of errors raised in the class.
+ 	* @var array
+ 	*/
+	
+	var $_errors = array();
+	
+	/**
+ 	* Constructor.
+ 	* @param string The page ID (urlname) of the page
+ 	* @param string The namespace of the page
+ 	* @param int Optional. The revision ID to send.
+ 	*/
+	
+	function __construct( $page_id, $namespace, $revision_id = 0 )
+	{
+		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'] )
+		{
+			$this->debug['works'] = true;
+			$this->debug['backtrace'] = enano_debug_print_backtrace(true);
+		}
+		
+		// First things first - check page existence and permissions
+		
+		if ( !isset($paths->nslist[$namespace]) )
+		{
+			$this->send_error('The namespace "' . htmlspecialchars($namespace) . '" does not exist.');
+		}
+		
+		if ( !is_int($revision_id) )
+			$revision_id = 0;
+		
+		$this->_setup( $page_id, $namespace, $revision_id );
+	}
+	
+	/**
+ 	* The main method to send the page content. Also responsible for checking permissions and calling the statistics counter.
+ 	* @param bool If true, the stat counter is called. Defaults to false.
+ 	*/
+	
+	function send( $do_stats = false )
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		global $lang, $output;
+		
+		profiler_log('PageProcessor: send() called');
+		
+		if ( !$this->perms->get_permissions('read') )
+		{
+			// Permission denied to read page. Is this one of our core pages that must always be allowed?
+			// NOTE: Not even the administration panel will work if ACLs deny access to it.
+			if ( $this->namespace == 'Special' && in_array($this->page_id, array('Login', 'Logout', 'LangExportJSON', 'CSS')) )
+			{
+				// Do nothing; allow execution to continue
+			}
+			else
+			{
+				// Page isn't whitelisted, behave as normal
+				$this->err_access_denied();
+				return false;
+			}
+		}
+		if ( $this->revision_id > 0 && !$this->perms->get_permissions('history_view') )
+		{
+			$this->err_access_denied();
+			return false;
+		}
+		
+		// Is there a custom function registered for handling this namespace?
+		// DEPRECATED (even though it only saw its way into one alpha release.)
+		if ( $proc = $paths->get_namespace_processor($this->namespace) )
+		{
+			// yes, just call that
+			// this is protected aggressively by the PathManager against overriding critical namespaces
+			return call_user_func($proc, $this);
+		}
+		
+		$pathskey = $paths->nslist[ $this->namespace ] . $this->page_id;
+		$strict_no_headers = false;
+		$admin_fail = false;
+		if ( $this->namespace == 'Admin' && strstr($this->page_id, '/') )
+		{
+			$this->page_id = substr($this->page_id, 0, strpos($this->page_id, '/'));
+			$funcname = "page_{$this->namespace}_{$this->page_id}";
+			if ( function_exists($funcname) )
+			{
+				$this->page_exists = true;
+			}
+		}
+		if ( isPage($pathskey) )
+		{
+			$cdata = $this->ns->get_cdata();
+			
+			if ( $cdata['special'] == 1 )
+			{
+				$this->send_headers = false;
+				$strict_no_headers = true;
+				$GLOBALS['output'] = new Output_Naked();
+			}
+			if ( isset($cdata['password']) )
+			{
+				if ( $cdata['password'] != '' && $cdata['password'] != sha1('') )
+				{
+					$password =& $cdata['password'];
+					if ( $this->password != $password )
+					{
+						$this->err_wrong_password();
+						return false;
+					}
+				}
+			}
+			if ( isset($cdata['require_admin']) && $cdata['require_admin'] )
+			{
+				if ( $session->auth_level < USER_LEVEL_ADMIN )
+				{
+					$admin_fail = true;
+				}
+			}
+		}
+		else if ( $this->namespace === $paths->namespace && $this->page_id == $paths->page_id )
+		{
+			if ( isset($paths->cpage['require_admin']) && $paths->cpage['require_admin'] )
+			{
+				if ( $session->auth_level < USER_LEVEL_ADMIN )
+				{
+					$admin_fail = true;
+				}
+			}
+		}
+		if ( $admin_fail )
+		{
+			header('Content-type: text/javascript');
+			echo enano_json_encode(array(
+					'mode' => 'error',
+					'error' => 'need_auth_to_admin'
+				));
+			return true;
+		}
+		if ( $this->page_exists && $this->namespace != 'Special' && $this->namespace != 'Admin' && $do_stats )
+		{
+			require_once(ENANO_ROOT.'/includes/stats.php');
+			doStats($this->page_id, $this->namespace);
+		}
+		
+		// We are all done. Ship off the page.
+		
+		if ( !$this->allow_redir )
+		{
+			if ( method_exists($this->ns, 'get_redirect') )
+			{
+				if ( $result = $this->ns->get_redirect() )
+					display_redirect_notice($result['page_id'], $result['namespace']);
+			}
+		}
+		else
+		{
+			$this->process_redirects();
+			
+			if ( count($this->redirect_stack) > 0 )
+			{
+				$stack = array_reverse($this->redirect_stack);
+				foreach ( $stack as $stackel )
+				{
+					$url = makeUrlNS($stackel['old_namespace'], $stackel['old_page_id'], 'redirect=no', true);
+					$page_data = $this->ns->get_cdata();
+					$title = $stackel['old_title'];
+					$a = '<a href="' . $url . '">' . htmlspecialchars($title) . '</a>';
+					$output->add_after_header('<small>' . $lang->get('page_msg_redirected_from', array('from' => $a)) . '<br /></small>');
+				}
+				$template->set_page($this);
+			}
+			
+			if ( $this->redir_error )
+			{
+				$output->add_after_header('<div class="usermessage"><b>' . $this->redir_error . '</b></div>');
+				$result = $this->ns->get_redirect();
+				display_redirect_notice($result['page_id'], $result['namespace']);
+			}
+		}
+		
+		$this->ns->send();
+	}
+	
+	/**
+ 	* Sends the page through by fetching it from the database.
+ 	*/
+ 	
+	function send_from_db($strict_no_headers = false)
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		global $lang;
+		
+		$this->ns->send_from_db();
+	}
+	
+	/**
+ 	* Fetches the wikitext or HTML source for the page.
+ 	* @return string
+ 	*/
+	
+	function fetch_source()
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		
+		if ( !$this->perms->get_permissions('view_source') )
+		{
+			return false;
+		}
+		if ( !$this->page_exists )
+		{
+			return '';
+		}
+		$cdata = $this->ns->get_cdata();
+		if ( isset($cdata['password']) )
+		{
+			if ( $cdata['password'] != sha1('') && $cdata['password'] !== $this->password && !empty($cdata['password']) )
+			{
+				return false;
+			}
+		}
+		return $this->fetch_text();
+	}
+	
+	/**
+ 	* Updates (saves/changes/edits) the content of the page.
+ 	* @param string The new text for the page
+ 	* @param string A summary of edits made to the page.
+ 	* @param bool If true, the edit is marked as a minor revision
+ 	* @param string Page format - wikitext or xhtml. REQUIRED, and new in 1.1.6.
+ 	* @return bool True on success, false on failure. When returning false, it will push errors to the PageProcessor error stack; read with $page->pop_error()
+ 	*/
+	
+	function update_page($text, $edit_summary = false, $minor_edit = false, $page_format)
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		global $lang;
+		
+		// Create the page if it doesn't exist
+		if ( !$this->page_exists )
+		{
+			if ( !$this->create_page() )
+			{
+				return false;
+			}
+		}
+			
+		//
+		// Validation
+		//
+		
+		$page_id = $db->escape($this->page_id);
+		$namespace = $db->escape($this->namespace);
+		
+		$q = $db->sql_query('SELECT protected FROM ' . table_prefix . "pages WHERE urlname='$page_id' AND namespace='$namespace';");
+		if ( !$q )
+			$db->_die('PageProcess updating page content');
+		if ( $db->numrows() < 1 )
+		{
+			$this->raise_error($lang->get('editor_err_no_rows'));
+			return false;
+		}
+		
+		// Do we have permission to edit the page?
+		if ( !$this->perms->get_permissions('edit_page') )
+		{
+			$this->raise_error($lang->get('editor_err_no_permission'));
+			return false;
+		}
+		
+		list($protection) = $db->fetchrow_num();
+		$db->free_result();
+		
+		if ( $protection == 1 )
+		{
+			// The page is protected - do we have permission to edit protected pages?
+			if ( !$this->perms->get_permissions('even_when_protected') )
+			{
+				$this->raise_error($lang->get('editor_err_page_protected'));
+				return false;
+			}
+		}
+		else if ( $protection == 2 )
+		{
+			// The page is semi-protected.
+			if (
+ 					( !$session->user_logged_in || // Is the user logged in?
+ 						( $session->user_logged_in && $session->reg_time + ( 4 * 86400 ) >= time() ) ) // If so, have they been registered for 4 days?
+ 					&& !$this->perms->get_permissions('even_when_protected') ) // And of course, is there an ACL that overrides semi-protection?
+			{
+				$this->raise_error($lang->get('editor_err_page_protected'));
+				return false;
+			}
+		}
+		
+		// Spam check
+		if ( !spamalyze($text) )
+		{
+			$this->raise_error($lang->get('editor_err_spamcheck_failed'));
+			return false;
+		}
+		
+		// Page format check
+		if ( !in_array($page_format, array('xhtml', 'wikitext')) )
+		{
+			$this->raise_error("format \"$page_format\" not one of [xhtml, wikitext]");
+			return false;
+		}
+		
+		//
+		// Protection validated; update page content
+		//
+		
+		$text_undb = RenderMan::preprocess_text($text, false, false);
+		$text = $db->escape($text_undb);
+		$author = $db->escape($session->username);
+		$time = time();
+		$edit_summary = ( strval($edit_summary) === $edit_summary ) ? $db->escape($edit_summary) : '';
+		$minor_edit = ( $minor_edit ) ? '1' : '0';
+		$date_string = enano_date(ED_DATE | ED_TIME);
+		
+		// Insert log entry
+		$sql = 'INSERT INTO ' . table_prefix . "logs ( time_id, date_string, log_type, action, page_id, namespace, author, author_uid, page_text, edit_summary, minor_edit, page_format )\n"
+ 				. "  VALUES ( $time, '$date_string', 'page', 'edit', '{$this->page_id}', '{$this->namespace}', '$author', $session->user_id, '$text', '$edit_summary', $minor_edit, '$page_format' );";
+		if ( !$db->sql_query($sql) )
+		{
+			$this->raise_error($db->get_error());
+			return false;
+		}
+		
+		// Update the master text entry
+		$sql = 'UPDATE ' . table_prefix . "page_text SET page_text = '$text' WHERE page_id = '{$this->page_id}' AND namespace = '{$this->namespace}';";
+		if ( !$db->sql_query($sql) )
+		{
+			$this->raise_error($db->get_error());
+			return false;
+		}
+		
+		// If there's an identical draft copy, delete it
+		$sql = 'DELETE FROM ' . table_prefix . "logs WHERE is_draft = 1 AND page_id = '{$this->page_id}' AND namespace = '{$this->namespace}' AND page_text = '{$text}';";
+		if ( !$db->sql_query($sql) )
+		{
+			$this->raise_error($db->get_error());
+			return false;
+		}
+		
+		// Set page_format
+		// Using @ due to warning thrown when saving new page
+		$cdata = $this->ns->get_cdata();
+		if ( @$cdata['page_format'] !== $page_format )
+		{
+			// Note: no SQL injection to worry about here. Everything that goes into this is sanitized already, barring some rogue plugin.
+			// (and if there's a rogue plugin running, we have bigger things to worry about anyway.)
+			if ( !$db->sql_query('UPDATE ' . table_prefix . "pages SET page_format = '$page_format' WHERE urlname = '$this->page_id' AND namespace = '$this->namespace';") )
+			{
+				$this->raise_error($db->get_error());
+				return false;
+			}
+			$paths->update_metadata_cache();
+		}
+		
+		// Rebuild the search index
+		$paths->rebuild_page_index($this->page_id, $this->namespace);
+		
+		$this->text_cache = $text_undb;
+		
+		return true;
+		
+	}
+	
+	/**
+ 	* Creates the page if it doesn't already exist.
+ 	* @param string Optional page title.
+ 	* @param bool Visibility (allow indexing) flag
+ 	* @return bool True on success, false on failure.
+ 	*/
+	
+	function create_page($title = false, $visible = true)
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		global $lang;
+		
+		// Do we have permission to create the page?
+		if ( !$this->perms->get_permissions('create_page') )
+		{
+			$this->raise_error($lang->get('pagetools_create_err_no_permission'));
+			return false;
+		}
+		
+		// Does it already exist?
+		if ( $this->page_exists )
+		{
+			$this->raise_error($lang->get('pagetools_create_err_already_exists'));
+			return false;
+		}
+		
+		// It's not in there. Perform validation.
+		
+		// We can't create special, admin, or external pages.
+		if ( $this->namespace == 'Special' || $this->namespace == 'Admin' || $this->namespace == 'API' )
+		{
+			$this->raise_error($lang->get('pagetools_create_err_nodb_namespace'));
+			return false;
+		}
+		
+		// Guess the proper title
+		$name = ( !empty($title) ) ? $title : str_replace('_', ' ', dirtify_page_id($this->page_id));
+		
+		// Check for the restricted Project: prefix
+		if ( substr($this->page_id, 0, 8) == 'Project:' )
+		{
+			$this->raise_error($lang->get('pagetools_create_err_reserved_prefix'));
+			return false;
+		}
+		
+		// Validation successful - insert the page
+		
+		$metadata = array(
+				'urlname' => $this->page_id,
+				'namespace' => $this->namespace,
+				'name' => $name,
+				'special' => 0,
+				'visible' => $visible ? 1 : 0,
+				'comments_on' => 1,
+				'protected' => ( $this->namespace == 'System' ? 1 : 0 ),
+				'delvotes' => 0,
+				'delvote_ips' => serialize(array()),
+				'wiki_mode' => 2
+			);
+		
+		$paths->add_page($metadata);
+		
+		$page_id = $db->escape($this->page_id);
+		$namespace = $db->escape($this->namespace);
+		$name = $db->escape($name);
+		$protect = ( $this->namespace == 'System' ) ? '1' : '0';
+		$blank_array = $db->escape(serialize(array()));
+		
+		// Query 1: Metadata entry
+		$q = $db->sql_query('INSERT INTO ' . table_prefix . "pages(name, urlname, namespace, visible, protected, delvotes, delvote_ips, wiki_mode)\n"
+											. "  VALUES ( '$name', '$page_id', '$namespace', {$metadata['visible']}, $protect, 0, '$blank_array', 2 );");
+		if ( !$q )
+			$db->_die('PageProcessor page creation - metadata stage');
+		
+		// Query 2: Text insertion
+		$q = $db->sql_query('INSERT INTO ' . table_prefix . "page_text(page_id, namespace, page_text)\n"
+												. "VALUES ( '$page_id', '$namespace', '' );");
+		if ( !$q )
+			$db->_die('PageProcessor page creation - text stage');
+		
+		// Query 3: Log entry
+		$db->sql_query('INSERT INTO ' . table_prefix."logs(time_id, date_string, log_type, action, author, author_uid, page_id, namespace)\n"
+ 									. "  VALUES ( " . time() . ", 'DEPRECATED', 'page', 'create', \n"
+ 									. "          '" . $db->escape($session->username) . "', $session->user_id, '" . $db->escape($this->page_id) . "', '" . $this->namespace . "');");
+		if ( !$q )
+			$db->_die('PageProcessor page creation - logging stage');
+		
+		// Update the cache
+		$paths->update_metadata_cache();
+		
+		// Make sure that when/if we save the page later in this instance it doesn't get re-created
+		$this->page_exists = true;
+		
+		// Page created. We're good!
+		return true;
+	}
+	
+	/**
+ 	* Rolls back a non-edit action in the logs
+ 	* @param int Log entry (log_id) to roll back
+ 	* @return array Standard Enano error/success protocol
+ 	*/
+	
+	function rollback_log_entry($log_id)
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		global $cache;
+		
+		// Verify permissions
+		if ( !$this->perms->get_permissions('history_rollback') )
+		{
+			return array(
+				'success' => false,
+				'error' => 'access_denied'
+				);
+		}
+		
+		// Check input
+		$log_id = intval($log_id);
+		if ( empty($log_id) )
+		{
+			return array(
+				'success' => false,
+				'error' => 'invalid_parameter'
+				);
+		}
+		
+		// Fetch the log entry
+		$q = $db->sql_query('SELECT * FROM ' . table_prefix . "logs WHERE log_type = 'page' AND page_id='{$this->page_id}' AND namespace='{$this->namespace}' AND log_id = $log_id;");
+		if ( !$q )
+			$db->_die();
+		
+		// Is this even a valid log entry for this context?
+		if ( $db->numrows() < 1 )
+		{
+			return array(
+				'success' => false,
+				'error' => 'entry_not_found'
+				);
+		}
+		
+		// All good, fetch and free the result
+		$log_entry = $db->fetchrow();
+		$db->free_result();
+		
+		$dateline = enano_date(ED_DATE | ED_TIME, $log_entry['time_id']);
+		
+		// Let's see, what do we have here...
+		switch ( $log_entry['action'] )
+		{
+			case 'rename':
+				// Page was renamed, let the rename method handle this
+				return array_merge($this->rename_page($log_entry['edit_summary']), array('dateline' => $dateline, 'action' => $log_entry['action']));
+				break;
+			case 'prot':
+			case 'unprot':
+			case 'semiprot':
+				return array_merge($this->protect_page(intval($log_entry['page_text']), '__REVERSION__'), array('dateline' => $dateline, 'action' => $log_entry['action']));
+				break;
+			case 'delete':
+				
+				// Raising a previously dead page has implications...
+				
+				// FIXME: l10n
+				// rollback_extra is required because usually only moderators can undo page deletion AND restore the content.
+				// potential flaw here - once recreated, can past revisions be restored by users without rollback_extra? should
+				// probably modify editor routine to deny revert access if the timestamp < timestamp of last deletion if any.
+				if ( !$this->perms->get_permissions('history_rollback_extra') )
+					return 'Administrative privileges are required for page undeletion.';
+				
+				// Rolling back the deletion of a page that was since created?
+				$pathskey = $paths->nslist[ $this->namespace ] . $this->page_id;
+				if ( isPage($pathskey) )
+					return array(
+							'success' => false,
+							// This is a clean Christian in-joke.
+							'error' => 'seeking_living_among_dead'
+						);
+				
+				// Generate a crappy page name
+				$name = $db->escape( str_replace('_', ' ', dirtify_page_id($this->page_id)) );
+				
+				// Stage 1 - re-insert page
+				$e = $db->sql_query('INSERT INTO ' . table_prefix.'pages(name,urlname,namespace) VALUES( \'' . $name . '\', \'' . $this->page_id . '\',\'' . $this->namespace . '\' )');
+				if ( !$e )
+					$db->die_json();
+				
+				// Select the latest published revision
+				$q = $db->sql_query('SELECT page_text FROM ' . table_prefix . "logs WHERE\n"
+													. "      log_type  = 'page'\n"
+													. "  AND action    = 'edit'\n"
+													. "  AND page_id   = '$this->page_id'\n"
+													. "  AND namespace = '$this->namespace'\n"
+													. "  AND is_draft != 1\n"
+													. "ORDER BY time_id DESC LIMIT 1;");
+				if ( !$q )
+					$db->die_json();
+				list($page_text) = $db->fetchrow_num();
+				$db->free_result($q);
+				
+				// Apply the latest revision as the current page text
+				$page_text = $db->escape($page_text);
+				$e = $db->sql_query('INSERT INTO ' . table_prefix."page_text(page_id, namespace, page_text) VALUES\n"
+													. "  ( '$this->page_id', '$this->namespace', '$page_text' );");
+				if ( !$e )
+					$db->die_json();
+				
+				$cache->purge('page_meta');
+				
+				return array(
+						'success' => true,
+						'dateline' => $dateline,
+						'action' => $log_entry['action']
+					);
+				
+				break;
+			case 'reupload':
+				
+				// given a log id and some revision info, restore the old file.
+				// get the timestamp of the file before this one
+				$q = $db->sql_query('SELECT time_id, file_key, file_extension, filename, size, mimetype FROM ' . table_prefix . "files WHERE time_id < {$log_entry['time_id']} ORDER BY time_id DESC LIMIT 1;");
+				if ( !$q )
+					$db->_die();
+				
+				$row = $db->fetchrow();
+				$db->free_result();
+				
+				// If the file hasn't been renamed to the new format (omitting timestamp), do that now.
+				$fname = ENANO_ROOT . "/files/{$row['file_key']}_{$row['time_id']}{$row['file_extension']}";
+				if ( @file_exists($fname) )
+				{
+					// it's stored in the old format - rename
+					$fname_new = ENANO_ROOT . "/files/{$row['file_key']}{$row['file_extension']}";
+					if ( !@rename($fname, $fname_new) )
+					{
+						return array(
+							'success' => false,
+							'error' => 'rb_file_rename_failed',
+							'action' => $log_entry['action']
+							);
+					}
+				}
+				
+				// Insert a new file entry
+				$time = time();
+				$filename = $db->escape($row['filename']);
+				$mimetype = $db->escape($row['mimetype']);
+				$ext = $db->escape($row['file_extension']);
+				$key = $db->escape($row['file_key']);
+				
+				$q = $db->sql_query('INSERT INTO ' . table_prefix . "files ( time_id, page_id, filename, size, mimetype, file_extension, file_key ) VALUES\n"
+							. "  ( $time, '$this->page_id', '$filename', {$row['size']}, '$mimetype', '$ext', '$key' );");
+				if ( !$q )
+					$db->die_json();
+				
+				// add reupload log entry
+				$username = $db->escape($session->username);
+				$q = $db->sql_query('INSERT INTO ' . table_prefix . "logs ( log_type, action, time_id, page_id, namespace, author, author_uid, edit_summary ) VALUES\n"
+													. "  ( 'page', 'reupload', $time, '$this->page_id', '$this->namespace', '$username', $session->user_id, '__ROLLBACK__' )");
+				if ( !$q )
+					$db->die_json();
+				
+				return array(
+						'success' => true,
+						'dateline' => $dateline,
+						'action' => $log_entry['action']
+					);
+				
+				break;
+			case 'votereset':
+				if ( !$this->perms->get_permissions('history_rollback_extra') )
+					return 'Denied!';
+				
+				// pull existing vote data
+				$q = $db->sql_query('SELECT delvotes, delvote_ips FROM ' . table_prefix . "pages WHERE urlname = '$this->page_id' AND namespace = '$this->namespace';");
+				if ( !$q )
+					$db->_die();
+				
+				if ( $db->numrows() < 1 )
+					return array(
+							'success' => false,
+							'error' => 'page_not_exist',
+							'action' => $log_entry['action']
+						);
+					
+				list($curr_delvotes, $curr_delvote_ips) = $db->fetchrow_num();
+				$db->free_result();
+				
+				// merge with existing votes
+				$old_delvote_ips = unserialize($log_entry['page_text']);
+				$new_delvote_ips = unserialize($curr_delvote_ips);
+				$new_delvote_ips['u'] = array_unique(array_merge($new_delvote_ips['u'], $old_delvote_ips['u']));
+				$new_delvote_ips['ip'] = array_unique(array_merge($new_delvote_ips['ip'], $old_delvote_ips['ip']));
+				$new_delvotes = count($new_delvote_ips['ip']);
+				$new_delvote_ips = $db->escape(serialize($new_delvote_ips));
+				
+				// update pages table
+				$q = $db->sql_query('UPDATE ' . table_prefix . "pages SET delvotes = $new_delvotes, delvote_ips = '$new_delvote_ips' WHERE urlname = '$this->page_id' AND namespace = '$this->namespace';");
+				
+				$cache->purge('page_meta');
+				
+				return array(
+						'success' => true,
+						'dateline' => $dateline,
+						'action' => $log_entry['action']
+					);
+				break;
+			default:
+				
+				return array(
+						'success' => false,
+						'error' => 'rb_action_not_supported',
+						'action' => $log_entry['action']
+					);
+				
+				break;
+		}
+	}
+	
+	/**
+ 	* Renames the page
+ 	* @param string New name
+ 	* @return array Standard Enano error/success protocol
+ 	*/
+	
+	function rename_page($new_name)
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		
+		// Check permissions
+		if ( !$this->perms->get_permissions('rename') )
+		{
+			return array(
+				'success' => false,
+				'error' => 'access_denied'
+				);
+		}
+		
+		// If this is the same as the current name, return success
+		$page_name = get_page_title_ns($this->page_id, $this->namespace);
+		if ( $page_name === $new_name )
+		{
+			return array(
+				'success' => true
+				);
+		}
+		
+		// Make sure the name is valid
+		$new_name = trim($new_name);
+		if ( empty($new_name) )
+		{
+			return array(
+				'success' => false,
+				'error' => 'invalid_parameter'
+				);
+		}
+		
+		// Log the action
+		$username = $db->escape($session->username);
+		$page_name = $db->escape($page_name);
+		$time = time();
+		
+		$q = $db->sql_query('INSERT INTO ' . table_prefix . "logs ( log_type, action, page_id, namespace, author, author_uid, edit_summary, time_id, date_string ) VALUES\n"
+											. "  ( 'page', 'rename', '{$this->page_id}', '{$this->namespace}', '$username', $session->user_id, '$page_name', '$time', 'DATE_STRING COLUMN OBSOLETE, USE time_id' );");
+		if ( !$q )
+			$db->_die();
+		
+		// Not much to do but to rename it now
+		$new_name = $db->escape($new_name);
+		$q = $db->sql_query('UPDATE ' . table_prefix . "pages SET name = '$new_name' WHERE urlname = '{$this->page_id}' AND namespace = '{$this->namespace}';");
+		if ( !$q )
+			$db->_die();
+		
+		// Update the cache
+		$paths->update_metadata_cache();
+		
+		return array(
+			'success' => true
+			);
+	}
+	
+	/**
+ 	* Sets the protection level of the page
+ 	* @param int Protection level, one of PROTECT_{FULL,SEMI,NONE}
+ 	* @param string Reason for protection - required
+ 	*/
+	
+	function protect_page($protection_level, $reason)
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		global $cache;
+		
+		// Validate permissions
+		if ( !$this->perms->get_permissions('protect') )
+		{
+			return array(
+				'success' => false,
+				'error' => 'access_denied'
+				);
+		}
+		
+		// Validate re-auth
+		if ( !$session->sid_super )
+		{
+			return array(
+				'success' => false,
+				'error' => 'access_denied_need_reauth'
+				);
+		}
+		
+		// Validate input
+		$reason = trim($reason);
+		if ( !in_array($protection_level, array(PROTECT_NONE, PROTECT_FULL, PROTECT_SEMI)) || empty($reason) )
+		{
+			return array(
+				'success' => false,
+				'error' => 'invalid_parameter'
+				);
+		}
+		
+		// Retrieve page metadata
+		$metadata = $this->ns->get_cdata();
+		
+		// Log the action
+		$username = $db->escape($session->username);
+		$time = time();
+		$existing_protection = intval($metadata['protected']);
+		$reason = $db->escape($reason);
+		
+		if ( $existing_protection == $protection_level )
+		{
+			return array(
+				'success' => false,
+				'error' => 'protection_already_there'
+				);
+		}
+		
+		$action = '[ insanity ]';
+		switch($protection_level)
+		{
+			case PROTECT_FULL: $action = 'prot'; break;
+			case PROTECT_NONE: $action = 'unprot'; break;
+			case PROTECT_SEMI: $action = 'semiprot'; break;
+		}
+		
+		$sql = 'INSERT INTO ' . table_prefix . "logs ( log_type, action, page_id, namespace, author, author_uid, edit_summary, time_id, page_text, date_string ) VALUES\n"
+ 				. "  ( 'page', '$action', '{$this->page_id}', '{$this->namespace}', '$username', $author_uid, '$reason', '$time', '$existing_protection', 'DATE_STRING COLUMN OBSOLETE, USE time_id' );";
+		if ( !$db->sql_query($sql) )
+		{
+			$db->die_json();
+		}
+		
+		// Perform the actual protection
+		$q = $db->sql_query('UPDATE ' . table_prefix . "pages SET protected = $protection_level WHERE urlname = '{$this->page_id}' AND namespace = '{$this->namespace}';");
+		if ( !$q )
+			$db->die_json();
+		
+		$cache->purge('page_meta');
+		
+		return array(
+			'success' => true
+			);
+	}
+	
+	/**
+ 	* Sets internal variables.
+ 	* @access private
+ 	*/
+	
+	function _setup($page_id, $namespace, $revision_id)
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		
+		$page_id_cleaned = sanitize_page_id($page_id);
+		
+		$this->revision_id = $revision_id;
+		$this->page_id_unclean = dirtify_page_id($page_id);
+		
+		// resolve namespace
+		$this->ns = namespace_factory($page_id, $namespace, $this->revision_id);
+		$this->page_id =& $this->ns->page_id;
+		$this->namespace =& $this->ns->namespace;
+		
+		$this->perms = $session->fetch_page_acl( $page_id, $namespace );
+		
+		$this->page_exists = $this->ns->exists();
+		$this->title = get_page_title_ns($this->page_id, $this->namespace);
+		
+		profiler_log("PageProcessor [{$this->namespace}:{$this->page_id}]: Ran _setup()");
+	}
+	
+	/**
+ 	* Processes any redirects.
+ 	* @access private
+ 	*/
+	
+	function process_redirects()
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		global $output, $lang;
+		
+		$this->redirect_stack = array();
+		
+		if ( !method_exists($this->ns, 'get_redirect') )
+			return true;
+		
+		if ( !$this->allow_redir )
+			return true;
+		
+		$redirect_count = 0;
+		
+		while ( $result = $this->ns->get_redirect() )
+		{
+			if ( $result['namespace'] == 'Special' || $result['namespace'] == 'Admin' )
+			{
+				// Can't redirect to special/admin page
+				$this->redir_error = $lang->get('page_err_redirect_to_special');
+				break;
+			}
+			if ( $redirect_count == 3 )
+			{
+				// max of 3 internal redirects exceeded
+				$this->redir_error = $lang->get('page_err_redirects_exceeded');
+				break;
+			}
+			
+			$loop = false;
+			foreach ( $this->redirect_stack as $stackel )
+			{
+				if ( $result['page_id'] == $stackel['old_page_id'] && $result['namespace'] == $stackel['old_namespace'] )
+				{
+					$loop = true;
+					break;
+				}
+			}
+			
+			if ( $loop )
+			{
+				// redirect loop
+				$this->redir_error = $lang->get('page_err_redirect_infinite_loop');
+				break;
+			}
+			$new_ns = namespace_factory($result['page_id'], $result['namespace']);
+			if ( !$new_ns->exists() )
+			{
+				// new page doesn't exist
+				$this->redir_error = $lang->get('page_err_redirect_to_nonexistent');
+				break;
+			}
+			
+			// build stack entry
+			$stackel = array(
+					'page_id' => $result['page_id'],
+					'namespace' => $result['namespace'],
+					'old_page_id' => $this->page_id,
+					'old_namespace' => $this->namespace,
+					'old_title' => $this->ns->title
+				);
+			
+			// replace everything (perform the actual redirect)
+			$this->ns = $new_ns;
+			
+			$this->page_id =& $this->ns->page_id;
+			$this->namespace =& $this->ns->namespace;
+			
+			$this->redirect_stack[] = $stackel;
+			
+			$redirect_count++;
+		}
+	}
+		
+	/**
+ 	* Sends the page header, dependent on, of course, whether we're supposed to.
+ 	*/
+	
+	function header()
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		if ( $this->send_headers )
+			$template->header();
+	}
+	
+	/**
+ 	* Sends the page footer, dependent on, of course, whether we're supposed to.
+ 	*/
+	
+	function footer()
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		if ( $this->send_headers )
+			$template->footer();
+	}
+	
+	/**
+ 	* Fetches the raw, unfiltered page text.
+ 	* @access public
+ 	*/
+	
+	function fetch_text()
+	{
+		return $this->ns->fetch_text();
+	}
+	
+	/**
+ 	* Tells us if the page exists.
+ 	* @return bool
+ 	*/
+	
+	function exists()
+	{
+		return $this->ns->exists();
+	}
+	
+	/**
+ 	* Send the error message to the user that the access to this page is denied.
+ 	* @access private
+ 	*/
+	
+	function err_access_denied()
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		global $lang;
+		global $email;
+		
+		// Log it for crying out loud
+		$q = $db->sql_query('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,author_uid,edit_summary,page_text) VALUES(\'security\', \'illegal_page\', '.time().', \'DEPRECATED\', \''.$db->escape($session->username).'\', ' . $session->user_id . ', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\', \'' . $db->escape(serialize(array($this->page_id, $this->namespace))) . '\')');
+		
+		$ob = '';
+		//$template->tpl_strings['PAGE_NAME'] = 'Access denied';
+		$template->tpl_strings['PAGE_NAME'] = htmlspecialchars( $this->title );
+			
+		if ( $this->send_headers )
+		{
+			$ob .= $template->getHeader();
+		}
+		
+		if ( count($this->redirect_stack) > 0 )
+		{
+			$stack = array_reverse($this->redirect_stack);
+			foreach ( $stack as $oldtarget )
+			{
+				$url = makeUrlNS($oldtarget[1], $oldtarget[0], 'redirect=no', true);
+				$old_page = namespace_factory($oldtarget[0], $oldtarget[1]);
+				$page_data = $old_page->get_cdata();
+				$title = ( isset($page_data['name']) ) ? $page_data['name'] : $paths->nslist[$oldtarget[1]] . htmlspecialchars( str_replace('_', ' ', dirtify_page_id( $oldtarget[0] ) ) );
+				$a = '<a href="' . $url . '">' . $title . '</a>';
+				
+				$url = makeUrlNS($this->namespace, $this->page_id, 'redirect=no', true);
+				$page_data = $this->ns->get_cdata();
+				$title = ( isset($page_data['name']) ) ? $page_data['name'] : $paths->nslist[$this->namespace] . htmlspecialchars( str_replace('_', ' ', dirtify_page_id( $this->page_id ) ) );
+				$b = '<a href="' . $url . '">' . $title . '</a>';
+				
+				$ob .= '<small>' . $lang->get('page_msg_redirected_from_to', array('from' => $a, 'to' => $b)) . '<br /></small>';
+			}
+		}
+		
+		$email_link = $email->encryptEmail(getConfig('contact_email'), '', '', $lang->get('page_err_access_denied_siteadmin'));
+		
+		$ob .= "<h3>" . $lang->get('page_err_access_denied_title') . "</h3>";
+		$ob .= "<p>" . $lang->get('page_err_access_denied_body', array('site_administration' => $email_link)) . "</p>";
+		
+		if ( $this->send_headers )
+		{
+			$ob .= $template->getFooter();
+		}
+		echo $ob;
+	}
+	
+	/**
+ 	* Inform the user of an incorrect or absent password
+ 	* @access private
+ 	*/
+ 	
+	function err_wrong_password()
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		global $lang;
+		
+		$title = $lang->get('page_msg_passrequired_title');
+		$message = ( empty($this->password) ) ?
+ 								'<p>' . $lang->get('page_msg_passrequired') . '</p>' :
+ 								'<p>' . $lang->get('page_msg_pass_wrong') . '</p>';
+		$message .= '<form action="' . makeUrlNS($this->namespace, $this->page_id) . '" method="post">
+ 									<p>
+ 										<label>' . $lang->get('page_lbl_password') . ' <input name="pagepass" type="password" /></label>&nbsp;&nbsp;<input type="submit" value="' . $lang->get('page_btn_password_submit') . '" />
+ 									</p>
+ 								</form>';
+		if ( $this->send_headers )
+		{
+			$template->tpl_strings['PAGE_NAME'] = $title;
+			$template->header();
+			echo "$message";
+			$template->footer();
+		}
+		else
+		{
+			echo "<h2>$title</h2>
+						$message";
+		}
+	}
+	
+	/**
+ 	* Send the error message to the user complaining that there weren't any rows.
+ 	* @access private
+ 	*/
+	
+	function err_no_rows()
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		
+		$title = 'No text rows';
+		$message = 'While the page\'s existence was verified, there were no rows in the database that matched the query for the text. This may indicate a bug with the software; ask the webmaster for more information. The offending query was:<pre>' . $db->latest_query . '</pre>';
+		if ( $this->send_headers )
+		{
+			$template->tpl_strings['PAGE_NAME'] = $title;
+			$template->header();
+			echo "<p>$message</p>";
+			$template->footer();
+		}
+		else
+		{
+			echo "<h2>$title</h2>
+						<p>$message</p>";
+		}
+	}
+	
+	/**
+ 	* Send an error message and die. For debugging or critical technical errors only - nothing that would under normal circumstances be shown to the user.
+ 	* @param string Error message
+ 	* @param bool If true, send DBAL's debugging information as well
+ 	*/
+ 	
+	function send_error($message, $sql = false)
+	{
+		global $db, $session, $paths, $template, $plugins; // Common objects
+		global $lang;
+		
+		$content = "<p>$message</p>";
+		$template->tpl_strings['PAGE_NAME'] = $lang->get('page_msg_general_error');
+		
+		if ( $this->debug['works'] )
+		{
+			$content .= $this->debug['backtrace'];
+		}
+		
+		header('HTTP/1.1 500 Internal Server Error');
+		
+		$template->header();
+		echo $content;
+		$template->footer();
+		
+		$db->close();
+		
+		exit;
+		
+	}
+	
+	/**
+ 	* Raises an error.
+ 	* @param string Error string
+ 	*/
+ 	
+	function raise_error($string)
+	{
+		if ( !is_string($string) )
+			return false;
+		$this->_errors[] = $string;
+	}
+	
+	/**
+ 	* Retrieves the latest error from the error stack and returns it ('pops' the error stack)
+ 	* @return string
+ 	*/
+	
+	function pop_error()
+	{
+		if ( count($this->_errors) < 1 )
+			return false;
+		return array_pop($this->_errors);
+	}
+	
 } // class PageProcessor
 
 ?>