SECURITY: CSRF protection in Private Messaging, which is a really broken feature and should get the TinyMCE treatment. *sigh* Reported by Secunia.
<?php
/*
* Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
* pageprocess.php - intelligent retrieval of pages
* Copyright (C) 2006-2009 Dan Fuhry
*
* This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
*/
/**
* Class to handle fetching page text (possibly from a cache) and formatting it.
* As of 1.0.4, this also handles the fetching and editing of certain data for pages.
* @package Enano
* @subpackage UI
* @copyright 2007 Dan Fuhry
* @license GNU General Public License <http://www.gnu.org/licenses/gpl-2.0.html>
*/
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. New in 1.1.6; defaults to "wikitext"
* @param array Optional - the entire incoming request. Plugins can add their own data to it.
* @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 = 'wikitext', $raw_request = array())
{
global $db, $session, $paths, $template, $plugins; // Common objects
global $lang;
// Create the page if it doesn't exist
$page_just_created = false;
if ( !$this->page_exists )
{
if ( !$this->create_page() )
{
return false;
}
// This is just to tell plugins if a page was just created, or is being edited.
$page_just_created = true;
}
//
// 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
$page_format = $db->escape($page_format);
if ( !preg_match('/^[a-z0-9_]+$/', $page_format) )
{
$this->raise_error('Page format must match /^[a-z0-9_]+$/');
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);
// Allow stuff to be run when the page is saved
$code = $plugins->setHook('update_page');
foreach ( $code as $cmd )
{
eval($cmd);
}
// If a plugin raised an error, honor it.
if ( $this->_errors )
{
return false;
}
// 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', {$session->user_id}, '$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> <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
?>