diff -r de56132c008d -r bdac73ed481e includes/pageprocess.php --- 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 = '' . htmlspecialchars($title) . ''; - $output->add_after_header('' . $lang->get('page_msg_redirected_from', array('from' => $a)) . '
'); - } - $template->set_page($this); - } - - if ( $this->redir_error ) - { - $output->add_after_header('
' . $this->redir_error . '
'); - $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 = '' . $title . ''; - - $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 = '' . $title . ''; - - $ob .= '' . $lang->get('page_msg_redirected_from_to', array('from' => $a, 'to' => $b)) . '
'; - } - } - - $email_link = $email->encryptEmail(getConfig('contact_email'), '', '', $lang->get('page_err_access_denied_siteadmin')); - - $ob .= "

" . $lang->get('page_err_access_denied_title') . "

"; - $ob .= "

" . $lang->get('page_err_access_denied_body', array('site_administration' => $email_link)) . "

"; - - 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) ) ? - '

' . $lang->get('page_msg_passrequired') . '

' : - '

' . $lang->get('page_msg_pass_wrong') . '

'; - $message .= '
-

-    -

-
'; - if ( $this->send_headers ) - { - $template->tpl_strings['PAGE_NAME'] = $title; - $template->header(); - echo "$message"; - $template->footer(); - } - else - { - echo "

$title

- $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:
' . $db->latest_query . '
'; - if ( $this->send_headers ) - { - $template->tpl_strings['PAGE_NAME'] = $title; - $template->header(); - echo "

$message

"; - $template->footer(); - } - else - { - echo "

$title

-

$message

"; - } - } - - /** - * 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 = "

$message

"; - $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 = '' . htmlspecialchars($title) . ''; + $output->add_after_header('' . $lang->get('page_msg_redirected_from', array('from' => $a)) . '
'); + } + $template->set_page($this); + } + + if ( $this->redir_error ) + { + $output->add_after_header('
' . $this->redir_error . '
'); + $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 = '' . $title . ''; + + $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 = '' . $title . ''; + + $ob .= '' . $lang->get('page_msg_redirected_from_to', array('from' => $a, 'to' => $b)) . '
'; + } + } + + $email_link = $email->encryptEmail(getConfig('contact_email'), '', '', $lang->get('page_err_access_denied_siteadmin')); + + $ob .= "

" . $lang->get('page_err_access_denied_title') . "

"; + $ob .= "

" . $lang->get('page_err_access_denied_body', array('site_administration' => $email_link)) . "

"; + + 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) ) ? + '

' . $lang->get('page_msg_passrequired') . '

' : + '

' . $lang->get('page_msg_pass_wrong') . '

'; + $message .= '
+

+    +

+
'; + if ( $this->send_headers ) + { + $template->tpl_strings['PAGE_NAME'] = $title; + $template->header(); + echo "$message"; + $template->footer(); + } + else + { + echo "

$title

+ $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:
' . $db->latest_query . '
'; + if ( $this->send_headers ) + { + $template->tpl_strings['PAGE_NAME'] = $title; + $template->header(); + echo "

$message

"; + $template->footer(); + } + else + { + echo "

$title

+

$message

"; + } + } + + /** + * 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 = "

$message

"; + $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 ?>