includes/pageutils.php
author Dan Fuhry <dan@enanocms.org>
Thu, 28 Oct 2010 03:05:31 -0400
changeset 1308 f9bee9b125ee
parent 1252 e34c23a35dc9
permissions -rw-r--r--
Parser updates. Added the "styled" keyword to wikitables to allow them to be styled using the current theme's standard table skinning, and changes to how the image tag parser decides how to display an image (framed, inline or raw).

<?php

/*
 * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
 * Copyright (C) 2006-2009 Dan Fuhry
 * pageutils.php - a class that handles raw page manipulations, used mostly by AJAX requests or their old-fashioned form-based counterparts
 *
 * 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 PageUtils {
	
	/**
 	* Tell if a username is used or not.
 	* @param $name the name to check for
 	* @return string
 	*/
	
	public static function checkusername($name)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		$name = str_replace('_', ' ', $name);
		$q = $db->sql_query('SELECT username FROM ' . table_prefix.'users WHERE username=\'' . $db->escape(rawurldecode($name)) . '\'');
		if ( !$q )
		{
			die($db->get_error());
		}
		if ( $db->numrows() < 1)
		{
			$db->free_result(); return('good');
		}
		else
		{
			$db->free_result(); return('bad');
		}
	}
	
	/**
 	* Get the wiki formatting source for a page
 	* @param $page the full page id (Namespace:Pagename)
 	* @return string
 	* @todo (DONE) Make it require a password (just for security purposes)
 	*/
 	
	public static function getsource($page, $password = false)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		if ( !isPage($page) )
		{
			return '';
		}
		
		list($page_id, $namespace) = RenderMan::strToPageID($page);
		$ns = namespace_factory($page_id, $namespace);
		$cdata = $ns->get_cdata();
		
		if ( strlen($cdata['password']) == 40 )
		{
			if(!$password || ( $password != $cdata['password']))
			{
				return 'invalid_password';
			}
		}
		
		if(!$session->get_permissions('view_source')) // Dependencies handle this for us - this also checks for read privileges
			return 'access_denied';
		$pid = RenderMan::strToPageID($page);
		if($pid[1] == 'Special' || $pid[1] == 'Admin')
		{
			die('This type of page (' . $paths->nslist[$pid[1]] . ') cannot be edited because the page source code is not stored in the database.');
		}
		
		$e = $db->sql_query('SELECT page_text,char_tag FROM ' . table_prefix.'page_text WHERE page_id=\'' . $pid[0] . '\' AND namespace=\'' . $pid[1] . '\'');
		if ( !$e )
		{
			$db->_die('The page text could not be selected.');
		}
		if( $db->numrows() < 1 )
		{
			return ''; //$db->_die('There were no rows in the text table that matched the page text query.');
		}
		
		$r = $db->fetchrow();
		$db->free_result();
		$message = $r['page_text'];
		
		return htmlspecialchars($message);
	}
	
	/**
 	* DEPRECATED. Previously returned the full rendered contents of a page.
 	* @param $page the full page id (Namespace:Pagename)
 	* @param $send_headers true if the theme headers should be sent (still dependent on current page settings), false otherwise
 	* @return string
 	*/
	
	public static function getpage($page, $send_headers = false, $hist_id = false)
	{
		die('PageUtils->getpage is deprecated.');
	}
	
	/**
 	* Writes page data to the database, after verifying permissions and running the XSS filter
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @param $message the text to save
 	* @return string
 	*/
 	
	public static function savepage($page_id, $namespace, $message, $summary = 'No edit summary given', $minor = false)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		
		$page = new PageProcessor($page_id, $namespace);
		$cdata = $page->ns->get_cdata();
		return $page->update_page($message, $summary, $minor, $cdata['page_format']);
	}
	
	/**
 	* Creates a page, both in memory and in the database.
 	* @param string $page_id
 	* @param string $namespace
 	* @return bool true on success, false on failure
 	*/
	
	public static function createPage($page_id, $namespace, $name = false, $visible = 1)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		if(in_array($namespace, Array('Special', 'Admin')))
		{
			// echo '<b>Notice:</b> PageUtils::createPage: You can\'t create a special page in the database<br />';
			return 'You can\'t create a special page in the database';
		}
		
		if(!isset($paths->nslist[$namespace]))
		{
			// echo '<b>Notice:</b> PageUtils::createPage: Couldn\'t look up the namespace<br />';
			return 'Couldn\'t look up the namespace';
		}
		
		$pname = $paths->nslist[$namespace] . $page_id;
		if(isPage($pname))
		{
			// echo '<b>Notice:</b> PageUtils::createPage: Page already exists<br />';
			return 'Page already exists';
		}
		
		if(!$session->get_permissions('create_page'))
		{
			// echo '<b>Notice:</b> PageUtils::createPage: Not authorized to create pages<br />';
			return 'Not authorized to create pages';
		}
		
		if($session->user_level < USER_LEVEL_ADMIN && $namespace == 'System')
		{
			// echo '<b>Notice:</b> PageUtils::createPage: Not authorized to create system messages<br />';
			return 'Not authorized to create system messages';
		}
		
		if ( substr($page_id, 0, 8) == 'Project:' )
		{
			// echo '<b>Notice:</b> PageUtils::createPage: Prefix "Project:" is reserved<br />';
			return 'The prefix "Project:" is reserved for a parser shortcut; if a page was created using this prefix, it would not be possible to link to it.';
		}
		
		/*
		// Dunno why this was here. Enano can handle more flexible names than this...
		$regex = '#^([A-z0-9 _\-\.\/\!\@\(\)]*)$#is';
		if(!preg_match($regex, $name))
		{
			//echo '<b>Notice:</b> PageUtils::createPage: Name contains invalid characters<br />';
			return 'Name contains invalid characters';
		}
		*/
		
		$page_id = dirtify_page_id($page_id);
		
		if ( !$name )
			$name = str_replace('_', ' ', $page_id);
		
		$page_id = sanitize_page_id( $page_id );
		
		$prot = ( $namespace == 'System' ) ? 1 : 0;
		
		$ips = array(
			'ip' => array(),
			'u' => array()
			);
		
		$page_data = Array(
			'name'=>$name,
			'urlname'=>$page_id,
			'namespace'=>$namespace,
			'special'=>0,'visible'=>1,'comments_on'=>0,'protected'=>$prot,'delvotes'=>0,'delvote_ips'=>serialize($ips),'wiki_mode'=>2,
		);
		
		// die('PageUtils::createpage: Creating page with this data:<pre>' . print_r($page_data, true) . '</pre>');
		
		$paths->add_page($page_data);
		
		$qa = $db->sql_query('INSERT INTO ' . table_prefix.'pages(name,urlname,namespace,visible,protected,delvote_ips) VALUES(\'' . $db->escape($name) . '\', \'' . $db->escape($page_id) . '\', \'' . $namespace . '\', '. ( $visible ? '1' : '0' ) .', ' . $prot . ', \'' . $db->escape(serialize($ips)) . '\');');
		$qb = $db->sql_query('INSERT INTO ' . table_prefix.'page_text(page_id,namespace) VALUES(\'' . $db->escape($page_id) . '\', \'' . $namespace . '\');');
		$qc = $db->sql_query('INSERT INTO ' . table_prefix.'logs(time_id,date_string,log_type,action,author,author_uid,page_id,namespace) VALUES('.time().', \'DEPRECATED\', \'page\', \'create\', \'' . $session->username . '\', ' . $session->user_id . ', \'' . $db->escape($page_id) . '\', \'' . $namespace . '\');');
		
		if($qa && $qb && $qc)
			return 'good';
		else
		{
			return $db->get_error();
		}
	}
	
	/**
 	* Sets the protection level on a page.
 	* @param $page_id string the page ID
 	* @param $namespace string the namespace
 	* @param $level int level of protection - 0 is off, 1 is full, 2 is semi
 	* @param $reason string why the page is being (un)protected
 	* @return string - "good" on success, in all other cases, an error string (on query failure, calls $db->_die() )
 	*/
	public static function protect($page_id, $namespace, $level, $reason)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		
		$page = new PageProcessor($page_id, $namespace);
		return $page->protect_page($level, $reason);
	}
	
	/**
 	* Generates an HTML table with history information in it.
 	* @param string the page ID
 	* @param string the namespace
 	* @param string page password
 	* @return string
 	*/
	
	public static function histlist($page_id, $namespace, $password = false)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $lang;
		
		if(!$session->get_permissions('history_view'))
			return 'Access denied';
		
		ob_start();
		
		$pname = $paths->get_pathskey($page_id, $namespace);
		$ns = namespace_factory($page_id, $namespace);
		$cdata = $ns->get_cdata();
		
		if ( !isPage($pname) )
		{
			return 'DNE';
		}
		
		if ( isPage($pname) )
		{
			$password_exists = ( !empty($cdata['password']) && $cdata['password'] !== sha1('') );
			if ( $password_exists && $password !== $cdata['password'] )
			{
				return '<p>' . $lang->get('history_err_wrong_password') . '</p>';
			}
		}
		
		$wiki = ( ( $cdata['wiki_mode'] == 2 && getConfig('wiki_mode') == '1') || $cdata['wiki_mode'] == 1) ? true : false;
		$prot = ( ( $cdata['protected'] == 2 && $session->user_logged_in && $session->reg_time + 60*60*24*4 < time() ) || $cdata['protected'] == 1) ? true : false;
		
		$q = 'SELECT log_id,time_id,date_string,page_id,namespace,author,author_uid,u.username,edit_summary,minor_edit FROM ' . table_prefix . "logs AS l\n"
 			. "  LEFT JOIN " . table_prefix . "users AS u\n"
 			. "    ON ( u.user_id = l.author_uid OR u.user_id IS NULL )\n"
 			. "  WHERE log_type='page' AND action='edit' AND page_id='$page_id' AND namespace='$namespace' AND is_draft != 1 ORDER BY time_id DESC;";
		
		if ( !($q = $db->sql_query($q)) )
			$db->_die('The history data for the page "' . $paths->cpage['name'] . '" could not be selected.');
		
		echo $lang->get('history_page_subtitle') . '
					<h3>' . $lang->get('history_heading_edits') . '</h3>';
		$numrows = $db->numrows();
		if ( $numrows < 1 )
		{
			echo $lang->get('history_no_entries');
		}
		else
		{
			echo '<form action="'.makeUrlNS($namespace, $page_id, 'do=diff').'" onsubmit="ajaxHistDiff(); return false;" method="get">
						<input type="submit" value="' . $lang->get('history_btn_compare') . '" />
						' . ( urlSeparator == '&' ? '<input type="hidden" name="title" value="' . htmlspecialchars($paths->nslist[$namespace] . $page_id) . '" />' : '' ) . '
						' . ( $session->sid_super ? '<input type="hidden" name="auth"  value="' . $session->sid_super . '" />' : '') . '
						<input type="hidden" name="do" value="diff" />
						<br /><span>&nbsp;</span>
						<div class="tblholder">
						<table border="0" width="100%" cellspacing="1" cellpadding="4">
						<tr>
							<th colspan="2">' . $lang->get('history_col_diff') . '</th>
							<th>' . $lang->get('history_col_datetime') . '</th>
							<th>' . $lang->get('history_col_user') . '</th>
							<th>' . $lang->get('history_col_summary') . '</th>
							<th>' . $lang->get('history_col_minor') . '</th>
							<th colspan="3">' . $lang->get('history_col_actions') . '</th>
						</tr>'."\n"."\n";
			$cls = 'row2';
			$ticker = 0;
			
			while ( $r = $db->fetchrow($q) )
			{
				
				$ticker++;
				
				if($cls == 'row2') $cls = 'row1';
				else $cls = 'row2';
				
				echo '<tr>'."\n";
				
				// Diff selection
				if($ticker == 1)
				{
					$s1 = '';
					$s2 = 'checked="checked" ';
				}
				elseif($ticker == 2)
				{
					$s1 = 'checked="checked" ';
					$s2 = '';
				}
				else
				{
					$s1 = '';
					$s2 = '';
				}
				if($ticker > 1)        echo '<td class="' . $cls . '" style="padding: 0;"><input ' . $s1 . 'name="diff1" type="radio" value="' . $r['time_id'] . '" id="diff1_' . $r['time_id'] . '" class="clsDiff1Radio" onclick="selectDiff1Button(this);" /></td>'."\n"; else echo '<td class="' . $cls . '"></td>';
				if($ticker < $numrows) echo '<td class="' . $cls . '" style="padding: 0;"><input ' . $s2 . 'name="diff2" type="radio" value="' . $r['time_id'] . '" id="diff2_' . $r['time_id'] . '" class="clsDiff2Radio" onclick="selectDiff2Button(this);" /></td>'."\n"; else echo '<td class="' . $cls . '"></td>';
				
				// Date and time
				echo '<td class="' . $cls . '" style="white-space: nowrap;">' . enano_date(ED_DATE | ED_TIME, intval($r['time_id'])) . '</td class="' . $cls . '">'."\n";
				
				// User
				$real_username = $r['author_uid'] > 1 && !empty($r['username']) ? $r['username'] : $r['author'];
				$rank_info = $session->get_user_rank($r['author_uid']);
				if ( $session->get_permissions('mod_misc') && is_valid_ip($r['author']) && $r['author_uid'] == 1 )
				{
					$rc = ' style="cursor: pointer;" title="' . $lang->get('history_tip_rdns') . '" onclick="ajaxReverseDNS(this, \'' . $r['author'] . '\');"';
				}
				else
				{
					$rc = '';
				}
				echo '<td class="' . $cls . '"' . $rc . '><a href="'.makeUrlNS('User', sanitize_page_id($real_username)).'" ';
				if ( !isPage($paths->nslist['User'] . sanitize_page_id($real_username)) )
				{
					echo 'class="wikilink-nonexistent"';
				}
				echo 'style="' . $rank_info['rank_style'] . '">' . htmlspecialchars($real_username) . '</a></td class="' . $cls . '">'."\n";
				
				// Edit summary
				if ( $r['edit_summary'] == 'Automatic backup created when logs were purged' )
				{
					$r['edit_summary'] = $lang->get('history_summary_clearlogs');
				}
				echo '<td class="' . $cls . '">' . $r['edit_summary'] . '</td>'."\n";
				
				// Minor edit
				echo '<td class="' . $cls . '" style="text-align: center;">'. (( $r['minor_edit'] ) ? 'M' : '' ) .'</td>'."\n";
				
				// Actions!
				echo '<td class="' . $cls . '" style="text-align: center;"><a rel="nofollow" href="'.makeUrlNS($namespace, $page_id, 'oldid=' . $r['log_id']) . '" onclick="ajaxHistView(\'' . $r['log_id'] . '\'); return false;">' . $lang->get('history_action_view') . '</a></td>'."\n";
				echo '<td class="' . $cls . '" style="text-align: center;"><a rel="nofollow" href="'.makeUrl($paths->nslist['Special'].'Contributions/' . $r['author']) . '">' . $lang->get('history_action_contrib') . '</a></td>'."\n";
				echo '<td class="' . $cls . '" style="text-align: center;"><a rel="nofollow" href="'.makeUrlNS($namespace, $page_id, 'do=edit&amp;revid=' . $r['log_id']) . '" onclick="ajaxEditor(\'' . $r['log_id'] . '\'); return false;">' . $lang->get('history_action_restore') . '</a></td>'."\n";
				
				echo '</tr>'."\n"."\n";
				
			}
			echo '</table>
						</div>
						<br />
						<input type="hidden" name="do" value="diff" />
						<input type="submit" value="' . $lang->get('history_btn_compare') . '" />
						</form>
						<script type="text/javascript">if ( !KILL_SWITCH ) { buildDiffList(); }</script>';
		}
		$db->free_result();
		echo '<h3>' . $lang->get('history_heading_other') . '</h3>';
		
		$sql = 'SELECT log_id,action,time_id,date_string,page_id,namespace,author,author_uid,u.username,edit_summary,minor_edit FROM ' . table_prefix . "logs AS l\n"
 				. "  LEFT JOIN " . table_prefix . "users AS u\n"
 				. "    ON ( u.user_id = l.author_uid OR u.user_id IS NULL )\n"
 				. "  WHERE log_type='page' AND action!='edit' AND page_id='$page_id' AND namespace='$namespace' AND is_draft != 1 ORDER BY time_id DESC;";
		
		if ( !( $q = $db->sql_query($sql)) )
		{
			$db->_die('The history data for the page "' . htmlspecialchars($paths->cpage['name']) . '" could not be selected.');
		}
		if ( $db->numrows() < 1 )
		{
			echo $lang->get('history_no_entries');
		}
		else
		{
			
			echo '<div class="tblholder">
							<table border="0" width="100%" cellspacing="1" cellpadding="4"><tr>
								<th>' . $lang->get('history_col_datetime') . '</th>
								<th>' . $lang->get('history_col_user') . '</th>
								<th>' . $lang->get('history_col_minor') . '</th>
								<th>' . $lang->get('history_col_action_taken') . '</th>
								<th>' . $lang->get('history_col_extra') . '</th>
								<th colspan="2"></th>
							</tr>';
			$cls = 'row2';
			while($r = $db->fetchrow($q)) {
				
				if($cls == 'row2') $cls = 'row1';
				else $cls = 'row2';
				
				echo '<tr>';
				
				// Date and time
				echo '<td class="' . $cls . '">' . enano_date(ED_DATE | ED_TIME, intval($r['time_id'])) . '</td class="' . $cls . '">';
				
				// User
				$real_username = $r['author_uid'] > 1 && !empty($r['username']) ? $r['username'] : $r['author'];
				$rank_info = $session->get_user_rank($r['author_uid']);
				if ( $session->get_permissions('mod_misc') && is_valid_ip($r['author']) && $r['author_uid'] == 1 )
				{
					$rc = ' style="cursor: pointer;" title="' . $lang->get('history_tip_rdns') . '" onclick="ajaxReverseDNS(this, \'' . $r['author'] . '\');"';
				}
				else
				{
					$rc = '';
				}
				echo '<td class="' . $cls . '"' . $rc . '><a href="'.makeUrlNS('User', sanitize_page_id($real_username)).'" ';
				if ( !isPage($paths->nslist['User'] . sanitize_page_id($real_username)) )
				{
					echo 'class="wikilink-nonexistent"';
				}
				echo 'style="' . $rank_info['rank_style'] . '">' . htmlspecialchars($real_username) . '</a></td class="' . $cls . '">'."\n";
				
				
				// Minor edit
				echo '<td class="' . $cls . '" style="text-align: center;">'. (( $r['minor_edit'] ) ? 'M' : '' ) .'</td>';
				
				// Action taken
				echo '<td class="' . $cls . '">';
				// Some of these are sanitized at insert-time. Others follow the newer Enano policy of stripping HTML at runtime.
				if    ($r['action']=='prot')     echo $lang->get('history_log_protect') . '</td><td class="' . $cls . '">'     . $lang->get('history_extra_reason') . ' ' . ( $r['edit_summary'] === '__REVERSION__' ? $lang->get('history_extra_protection_reversion') : htmlspecialchars($r['edit_summary']) );
				elseif($r['action']=='unprot')   echo $lang->get('history_log_unprotect') . '</td><td class="' . $cls . '">'   . $lang->get('history_extra_reason') . ' ' . ( $r['edit_summary'] === '__REVERSION__' ? $lang->get('history_extra_protection_reversion') : htmlspecialchars($r['edit_summary']) );
				elseif($r['action']=='semiprot') echo $lang->get('history_log_semiprotect') . '</td><td class="' . $cls . '">' . $lang->get('history_extra_reason') . ' ' . ( $r['edit_summary'] === '__REVERSION__' ? $lang->get('history_extra_protection_reversion') : htmlspecialchars($r['edit_summary']) );
				elseif($r['action']=='rename')   echo $lang->get('history_log_rename') . '</td><td class="' . $cls . '">' . $lang->get('history_extra_oldtitle') . ' '.htmlspecialchars($r['edit_summary']);
				elseif($r['action']=='create')   echo $lang->get('history_log_create') . '</td><td class="' . $cls . '">';
				elseif($r['action']=='delete')   echo $lang->get('history_log_delete') . '</td><td class="' . $cls . '">' . $lang->get('history_extra_reason') . ' ' . $r['edit_summary'];
				elseif($r['action']=='reupload') echo $lang->get('history_log_uploadnew') . '</td><td class="' . $cls . '">' . $lang->get('history_extra_reason') . ' ' . ( $r['edit_summary'] === '__ROLLBACK__' ? $lang->get('history_extra_upload_reversion') : htmlspecialchars($r['edit_summary']) );
				elseif($r['action']=='votereset')echo $lang->get('history_log_votereset') . '</td><td class="' . $cls . '">' . $lang->get('history_extra_numvotes') . ' ' . $r['edit_summary'];
				echo '</td>';
				
				// Actions!
				echo '<td class="' . $cls . '" style="text-align: center;"><a rel="nofollow" href="'.makeUrl($paths->nslist['Special'].'Contributions/' . $r['author']) . '">' . $lang->get('history_action_contrib') . '</a></td>';
				echo '<td class="' . $cls . '" style="text-align: center;"><a rel="nofollow" href="'.makeUrlNS($namespace, $page_id, 'do=rollback&amp;id=' . $r['log_id']) . '" onclick="ajaxRollback(\'' . $r['log_id'] . '\'); return false;">' . $lang->get('history_action_revert') . '</a></td>';
				
				echo '</tr>';
			}
			echo '</table></div>';
		}
		$db->free_result();
		$ret = ob_get_contents();
		ob_end_clean();
		return $ret;
	}
	
	/**
 	* Rolls back a logged action
 	* @param $id the time ID, a.k.a. the primary key in the logs table
 	* @return string
 	*/
 	
	public static function rollback($id)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $lang;
		
		// placeholder
		return 'PageUtils->rollback() is deprecated - use PageProcessor instead.';
	}
	
	/**
 	* Posts a comment.
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @param $name the name of the person posting, defaults to current username/IP
 	* @param $subject the subject line of the comment
 	* @param $text the comment text
 	* @return string javascript code
 	*/
 	
	public static function addcomment($page_id, $namespace, $name, $subject, $text, $captcha_code = false, $captcha_id = false)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		$_ob = '';
		if(!$session->get_permissions('post_comments'))
			return 'Access denied';
		if(getConfig('comments_need_login') == '2' && !$session->user_logged_in) _die('Access denied to post comments: you need to be logged in first.');
		if(getConfig('comments_need_login') == '1' && !$session->user_logged_in)
		{
			if(!$captcha_code || !$captcha_id) _die('BUG: PageUtils::addcomment: no CAPTCHA data passed to method');
			$result = $session->get_captcha($captcha_id);
			if(strtolower($captcha_code) != strtolower($result)) _die('The confirmation code you entered was incorrect.');
		}
		$text = RenderMan::preprocess_text($text);
		$name = $session->user_logged_in ? RenderMan::preprocess_text($session->username) : RenderMan::preprocess_text($name);
		$subj = RenderMan::preprocess_text($subject);
		if(getConfig('approve_comments', '0')=='1') $appr = '0'; else $appr = '1';
		$q = 'INSERT INTO ' . table_prefix.'comments(page_id,namespace,subject,comment_data,name,user_id,approved,time) VALUES(\'' . $page_id . '\',\'' . $namespace . '\',\'' . $subj . '\',\'' . $text . '\',\'' . $name . '\',' . $session->user_id . ',' . $appr . ','.time().')';
		$e = $db->sql_query($q);
		if(!$e) die('alert(unescape(\''.rawurlencode('Error inserting comment data: '.$db->get_error().'\n\nQuery:\n' . $q) . '\'))');
		else $_ob .= '<div class="info-box">Your comment has been posted.</div>';
		return PageUtils::comments($page_id, $namespace, false, Array(), $_ob);
	}
	
	/**
 	* Generates partly-compiled HTML/Javascript code to be eval'ed by the user's browser to display comments
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @param $action administrative action to perform, default is false
 	* @param $flags additional info for $action, shouldn't be used except when deleting/approving comments, etc.
 	* @param $_ob text to prepend to output, used by PageUtils::addcomment
 	* @return array
 	* @access private
 	*/
 	
	public static function comments_raw($page_id, $namespace, $action = false, $flags = Array(), $_ob = '')
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $lang;
		
		$pname = $paths->nslist[$namespace] . $page_id;
		$template->init_vars();
		
		ob_start();
		
		if($action && $session->get_permissions('mod_comments')) // Nip hacking attempts in the bud
		{
			switch($action) {
			case "delete":
				if(isset($flags['id']))
				{
					$q = 'DELETE FROM ' . table_prefix.'comments WHERE page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\' AND comment_id='.intval($flags['id']).' LIMIT 1;';
				} else {
					$n = $db->escape($flags['name']);
					$s = $db->escape($flags['subj']);
					$t = $db->escape($flags['text']);
					$q = 'DELETE FROM ' . table_prefix.'comments WHERE page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\' AND name=\'' . $n . '\' AND subject=\'' . $s . '\' AND comment_data=\'' . $t . '\' LIMIT 1;';
				}
				$e=$db->sql_query($q);
				if(!$e) die('alert(unesape(\''.rawurlencode('Error during query: '.$db->get_error().'\n\nQuery:\n' . $q) . '\'));');
				break;
			case "approve":
				if(isset($flags['id']))
				{
					$where = 'comment_id='.intval($flags['id']);
				} else {
					$n = $db->escape($flags['name']);
					$s = $db->escape($flags['subj']);
					$t = $db->escape($flags['text']);
					$where = 'name=\'' . $n . '\' AND subject=\'' . $s . '\' AND comment_data=\'' . $t . '\'';
				}
				$q = 'SELECT approved FROM ' . table_prefix.'comments WHERE page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\' AND ' . $where . ' LIMIT 1;';
				$e = $db->sql_query($q);
				if(!$e) die('alert(unesape(\''.rawurlencode('Error selecting approval status: '.$db->get_error().'\n\nQuery:\n' . $q) . '\'));');
				$r = $db->fetchrow();
				$db->free_result();
				$a = ( $r['approved'] ) ? '0' : '1';
				$q = 'UPDATE ' . table_prefix.'comments SET approved=' . $a . ' WHERE page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\' AND ' . $where . ';';
				$e=$db->sql_query($q);
				if(!$e) die('alert(unesape(\''.rawurlencode('Error during query: '.$db->get_error().'\n\nQuery:\n' . $q) . '\'));');
				if($a=='1') $v = $lang->get('comment_btn_mod_unapprove');
				else $v = $lang->get('comment_btn_mod_approve');
				echo 'document.getElementById("mdgApproveLink'.intval($_GET['id']).'").innerHTML="' . $v . '";';
				break;
			}
		}
		
		if(!defined('ENANO_TEMPLATE_LOADED'))
		{
			$template->load_theme($session->theme, $session->style);
		}
		
		$tpl = $template->makeParser('comment.tpl');
		
		$e = $db->sql_query('SELECT * FROM ' . table_prefix.'comments WHERE page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\' AND approved=0;');
		if(!$e) $db->_die('The comment text data could not be selected.');
		$num_unapp = $db->numrows();
		$db->free_result();
		$e = $db->sql_query('SELECT * FROM ' . table_prefix.'comments WHERE page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\' AND approved=1;');
		if(!$e) $db->_die('The comment text data could not be selected.');
		$num_app = $db->numrows();
		$db->free_result();
		$lq = $db->sql_query('SELECT c.comment_id,c.subject,c.name,c.comment_data,c.approved,c.time,c.user_id,c.ip_address,u.user_level,u.email,u.signature,u.user_has_avatar,u.avatar_type
									FROM ' . table_prefix.'comments AS c
									LEFT JOIN ' . table_prefix.'users AS u
										ON c.user_id=u.user_id
									WHERE page_id=\'' . $page_id . '\'
									AND namespace=\'' . $namespace . '\' ORDER BY c.time ASC;');
		if(!$lq) _die('The comment text data could not be selected. '.$db->get_error());
		$_ob .= '<h3>' . $lang->get('comment_heading') . '</h3>';
		
		$n = ( $session->get_permissions('mod_comments')) ? $db->numrows() : $num_app;
		
		$subst = array(
				'num_comments' => $n,
				'page_type' => $template->namespace_string
			);
		
		$_ob .= '<p>';
		$_ob .= ( $n == 0 ) ? $lang->get('comment_msg_count_zero', $subst) : ( $n == 1 ? $lang->get('comment_msg_count_one', $subst) : $lang->get('comment_msg_count_plural', $subst) );
		
		if ( $session->get_permissions('mod_comments') && $num_unapp > 0 )
		{
			$_ob .= ' <span style="color: #D84308">' . $lang->get('comment_msg_count_unapp_mod', array( 'num_unapp' => $num_unapp )) . '</span>';
		}
		else if ( !$session->get_permissions('mod_comments') && $num_unapp > 0 )
		{
			$ls = ( $num_unapp == 1 ) ? 'comment_msg_count_unapp_one' : 'comment_msg_count_unapp_plural';
			$_ob .= ' <span>' . $lang->get($ls, array( 'num_unapp' => $num_unapp )) . '</span>';
		}
		$_ob .= '</p>';
		$list = 'list = { ';
		// _die(htmlspecialchars($ttext));
		$i = -1;
		while ( $row = $db->fetchrow($lq) )
		{
			$i++;
			$strings = Array();
			$bool = Array();
			if ( $session->get_permissions('mod_comments') || $row['approved'] == COMMENT_APPROVED )
			{
				$list .= $i . ' : { \'comment\' : unescape(\''.rawurlencode($row['comment_data']).'\'), \'name\' : unescape(\''.rawurlencode($row['name']).'\'), \'subject\' : unescape(\''.rawurlencode($row['subject']).'\'), }, ';
				
				// Comment ID (used in the Javascript apps)
				$strings['ID'] = (string)$i;
				
				// Determine the name, and whether to link to the user page or not
				$name = '';
				if($row['user_id'] > 1) $name .= '<a href="'.makeUrlNS('User', sanitize_page_id(' ', '_', $row['name'])).'">';
				$name .= $row['name'];
				if($row['user_id'] > 1) $name .= '</a>';
				$strings['NAME'] = $name; unset($name);
				
				// Subject
				$s = $row['subject'];
				if(!$row['approved']) $s .= ' <span style="color: #D84308">' . $lang->get('comment_msg_note_unapp') . '</span>';
				$strings['SUBJECT'] = $s;
				
				// Date and time
				$strings['DATETIME'] = enano_date(ED_DATE | ED_TIME, $row['time']);
				
				// User level
				switch($row['user_level'])
				{
					default:
					case USER_LEVEL_GUEST:
						$l = $lang->get('user_type_guest');
						break;
					case USER_LEVEL_MEMBER:
					case USER_LEVEL_CHPREF:
						$l = $lang->get('user_type_member');
						break;
					case USER_LEVEL_MOD:
						$l = $lang->get('user_type_mod');
						break;
					case USER_LEVEL_ADMIN:
						$l = $lang->get('user_type_admin');
						break;
				}
				$strings['USER_LEVEL'] = $l; unset($l);
				
				// The actual comment data
				$strings['DATA'] = RenderMan::render($row['comment_data']);
				
				if($session->get_permissions('edit_comments'))
				{
					// Edit link
					$strings['EDIT_LINK'] = '<a href="'.makeUrlNS($namespace, $page_id, 'do=comments&amp;sub=editcomment&amp;id=' . $row['comment_id']) . '" id="editbtn_' . $i . '">' . $lang->get('comment_btn_edit') . '</a>';
				
					// Delete link
					$strings['DELETE_LINK'] = '<a href="'.makeUrlNS($namespace, $page_id, 'do=comments&amp;sub=deletecomment&amp;id=' . $row['comment_id']) . '">' . $lang->get('comment_btn_delete') . '</a>';
				}
				else
				{
					// Edit link
					$strings['EDIT_LINK'] = '';
				
					// Delete link
					$strings['DELETE_LINK'] = '';
				}
				
				// Send PM link
				$strings['SEND_PM_LINK'] = ( $session->user_logged_in && $row['user_id'] > 1 ) ? '<a href="'.makeUrlNS('Special', 'PrivateMessages/Compose/To/' . $row['name']) . '">' . $lang->get('comment_btn_send_privmsg') . '</a><br />' : '';
				
				// Add Buddy link
				$strings['ADD_BUDDY_LINK'] = ( $session->user_logged_in && $row['user_id'] > 1 ) ? '<a href="'.makeUrlNS('Special', 'PrivateMessages/FriendList/Add/' . $row['name']) . '">' . $lang->get('comment_btn_add_buddy') . '</a>' : '';
				
				// Mod links
				$applink = '';
				$applink .= '<a href="'.makeUrlNS($namespace, $page_id, 'do=comments&amp;sub=admin&amp;action=approve&amp;id=' . $row['comment_id']) . '" id="mdgApproveLink' . $i . '">';
				if($row['approved']) $applink .= $lang->get('comment_btn_mod_unapprove');
				else $applink .= $lang->get('comment_btn_mod_approve');
				$applink .= '</a>';
				$strings['MOD_APPROVE_LINK'] = $applink; unset($applink);
				$strings['MOD_DELETE_LINK'] = '<a href="'.makeUrlNS($namespace, $page_id, 'do=comments&amp;sub=admin&amp;action=delete&amp;id=' . $row['comment_id']) . '">' . $lang->get('comment_btn_mod_delete') . '</a>';
				$strings['MOD_IP_LINK'] = '<span style="opacity: 0.5; filter: alpha(opacity=50);">' . ( ( empty($row['ip_address']) ) ? $lang->get('comment_btn_mod_ip_missing') : $lang->get('comment_btn_mod_ip_notimplemented') ) . '</span>';
				
				// Signature
				$strings['SIGNATURE'] = '';
				if($row['signature'] != '') $strings['SIGNATURE'] = RenderMan::render($row['signature']);
				
				// Avatar
				if ( $row['user_has_avatar'] == 1 )
				{
					$bool['user_has_avatar'] = true;
					$strings['AVATAR_ALT'] = $lang->get('usercp_avatar_image_alt', array('username' => $row['name']));
					$strings['AVATAR_URL'] = make_avatar_url(intval($row['user_id']), $row['avatar_type'], $row['email']);
					$strings['USERPAGE_LINK'] = makeUrlNS('User', $row['name']);
				}
				
				$bool['auth_mod'] = ($session->get_permissions('mod_comments')) ? true : false;
				$bool['can_edit'] = ( ( $session->user_logged_in && $row['name'] == $session->username && $session->get_permissions('edit_comments') ) || $session->get_permissions('mod_comments') ) ? true : false;
				$bool['signature'] = ( $strings['SIGNATURE'] == '' ) ? false : true;
				
				// Done processing and compiling, now let's cook it into HTML
				$tpl->assign_vars($strings);
				$tpl->assign_bool($bool);
				$_ob .= $tpl->run();
			}
		}
		if(getConfig('comments_need_login') != '2' || $session->user_logged_in)
		{
			if($session->get_permissions('post_comments'))
			{
				$_ob .= '<h3>' . $lang->get('comment_postform_title') . '</h3>';
				$_ob .= $lang->get('comment_postform_blurb');
				if(getConfig('approve_comments', '0')=='1') $_ob .= ' ' . $lang->get('comment_postform_blurb_unapp');
				if(getConfig('comments_need_login') == '1' && !$session->user_logged_in)
				{
					$_ob .= ' ' . $lang->get('comment_postform_blurb_captcha');
				}
				$sn = $session->user_logged_in ? $session->username . '<input name="name" id="mdgScreenName" type="hidden" value="' . $session->username . '" />' : '<input name="name" id="mdgScreenName" type="text" size="35" />';
				$_ob .= '  <a href="#" id="mdgCommentFormLink" style="display: none;" onclick="document.getElementById(\'mdgCommentForm\').style.display=\'block\';this.style.display=\'none\';return false;">' . $lang->get('comment_postform_blurb_link') . '</a>
				<div id="mdgCommentForm">
				<form action="'.makeUrlNS($namespace, $page_id, 'do=comments&amp;sub=postcomment').'" method="post" style="margin-left: 1em">
				<table border="0">
				<tr><td>' . $lang->get('comment_postform_field_name') . '</td><td>' . $sn . '</td></tr>
				<tr><td>' . $lang->get('comment_postform_field_subject') . '</td><td><input name="subj" id="mdgSubject" type="text" size="35" /></td></tr>';
				if(getConfig('comments_need_login') == '1' && !$session->user_logged_in)
				{
					$session->kill_captcha();
					$captcha = $session->make_captcha();
					$_ob .= '<tr><td>' . $lang->get('comment_postform_field_captcha_title') . '<br /><small>' . $lang->get('comment_postform_field_captcha_blurb') . '</small></td><td><img src="'.makeUrlNS('Special', 'Captcha/' . $captcha) . '" alt="Visual confirmation" style="cursor: pointer;" onclick="this.src = \''.makeUrlNS("Special", "Captcha/".$captcha).'/\'+Math.floor(Math.random() * 100000);" /><input name="captcha_id" id="mdgCaptchaID" type="hidden" value="' . $captcha . '" /><br />' . $lang->get('comment_postform_field_captcha_label') . ' <input name="captcha_input" id="mdgCaptchaInput" type="text" size="10" /><br /><small><script type="text/javascript">document.write("' . $lang->get('comment_postform_field_captcha_cantread_js') . '");</script><noscript>' . $lang->get('comment_postform_field_captcha_cantread_nojs') . '</noscript></small></td></tr>';
				}
				$_ob .= '
				<tr><td valign="top">' . $lang->get('comment_postform_field_comment') . '</td><td><textarea name="text" id="mdgCommentArea" rows="10" cols="40"></textarea></td></tr>
				<tr><td colspan="2" style="text-align: center;"><input type="submit" value="' . $lang->get('comment_postform_btn_submit') . '" /></td></tr>
				</table>
				</form>
				</div>';
			}
		} else {
			// FIXME: l10n
			$_ob .= '<h3>' . $lang->get('comment_postform_title') . '</h3><p>You need to be logged in to post comments. <a href="'.makeUrlNS('Special', 'Login/' . $pname . '%2523comments').'">Log in</a></p>';
		}
		$list .= '};';
		echo 'document.getElementById(\'ajaxEditContainer\').innerHTML = unescape(\''. rawurlencode($_ob) .'\');
		' . $list;
		echo 'Fat.fade_all(); document.getElementById(\'mdgCommentForm\').style.display = \'none\'; document.getElementById(\'mdgCommentFormLink\').style.display="inline";';
		
		$ret = ob_get_contents();
		ob_end_clean();
		return Array($ret, $_ob);
		
	}
	
	/**
 	* Generates ready-to-execute Javascript code to be eval'ed by the user's browser to display comments
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @param $action administrative action to perform, default is false
 	* @param $flags additional info for $action, shouldn't be used except when deleting/approving comments, etc.
 	* @param $_ob text to prepend to output, used by PageUtils::addcomment
 	* @return string
 	*/
 	
	public static function comments($page_id, $namespace, $action = false, $id = -1, $_ob = '')
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		$r = PageUtils::comments_raw($page_id, $namespace, $action, $id, $_ob);
		return $r[0];
	}
	
	/**
 	* Generates HTML code for comments - used in browser compatibility mode
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @param $action administrative action to perform, default is false
 	* @param $flags additional info for $action, shouldn't be used except when deleting/approving comments, etc.
 	* @param $_ob text to prepend to output, used by PageUtils::addcomment
 	* @return string
 	*/
	
	public static function comments_html($page_id, $namespace, $action = false, $id = -1, $_ob = '')
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		$r = PageUtils::comments_raw($page_id, $namespace, $action, $id, $_ob);
		return $r[1];
	}
	
	/**
 	* Updates comment data.
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @param $subject new subject
 	* @param $text new text
 	* @param $old_subject the old subject, unprocessed and identical to the value in the DB
 	* @param $old_text the old text, unprocessed and identical to the value in the DB
 	* @param $id the javascript list ID, used internally by the client-side app
 	* @return string
 	*/
	
	public static function savecomment($page_id, $namespace, $subject, $text, $old_subject, $old_text, $id = -1)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		if(!$session->get_permissions('edit_comments'))
			return 'result="BAD";error="Access denied"';
		// Avoid SQL injection
		$old_text    = $db->escape($old_text);
		$old_subject = $db->escape($old_subject);
		// Safety check - username/login
		if(!$session->get_permissions('mod_comments')) // allow mods to edit comments
		{
			if(!$session->user_logged_in) _die('AJAX comment save safety check failed because you are not logged in. Sometimes this can happen because you are using a browser that does not send cookies as part of AJAX requests.<br /><br />Please log in and try again.');
			$q = 'SELECT c.name FROM ' . table_prefix.'comments c, ' . table_prefix.'users u WHERE comment_data=\'' . $old_text . '\' AND subject=\'' . $old_subject . '\' AND page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\' AND u.user_id=c.user_id;';
			$s = $db->sql_query($q);
			if(!$s) _die('SQL error during safety check: '.$db->get_error().'<br /><br />Attempted SQL:<br /><pre>'.htmlspecialchars($q).'</pre>');
			$r = $db->fetchrow($s);
			$db->free_result();
			if($db->numrows() < 1 || $r['name'] != $session->username) _die('Safety check failed, probably due to a hacking attempt.');
		}
		$s = RenderMan::preprocess_text($subject);
		$t = RenderMan::preprocess_text($text);
		$sql  = 'UPDATE ' . table_prefix.'comments SET subject=\'' . $s . '\',comment_data=\'' . $t . '\' WHERE comment_data=\'' . $old_text . '\' AND subject=\'' . $old_subject . '\' AND page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\'';
		$result = $db->sql_query($sql);
		if($result)
		{
			return 'result="GOOD";
											list[' . $id . '][\'subject\'] = unescape(\''.str_replace('%5Cn', '%0A', rawurlencode(str_replace('{{EnAnO:Newline}}', '\\n', stripslashes(str_replace('\\n', '{{EnAnO:Newline}}', $s))))).'\');
											list[' . $id . '][\'comment\'] = unescape(\''.str_replace('%5Cn', '%0A', rawurlencode(str_replace('{{EnAnO:Newline}}', '\\n', stripslashes(str_replace('\\n', '{{EnAnO:Newline}}', $t))))).'\'); id = ' . $id . ';
			s = unescape(\''.rawurlencode($s).'\');
			t = unescape(\''.str_replace('%5Cn', '<br \\/>', rawurlencode(RenderMan::render(str_replace('{{EnAnO:Newline}}', "\n", stripslashes(str_replace('\\n', '{{EnAnO:Newline}}', $t)))))).'\');';
		}
		else
		{
			return 'result="BAD"; error=unescape("'.rawurlencode('Enano encountered a problem whilst saving the comment.
			Performed SQL:
			' . $sql . '
		
			Error returned by MySQL: '.$db->get_error()).'");';
		}
	}
	
	/**
 	* Updates comment data using the comment_id column instead of the old, messy way
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @param $subject new subject
 	* @param $text new text
 	* @param $id the comment ID (primary key in enano_comments table)
 	* @return string
 	*/
	
	public static function savecomment_neater($page_id, $namespace, $subject, $text, $id)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		if(!is_int($id)) die('PageUtils::savecomment: $id is not an integer, aborting for safety');
		if(!$session->get_permissions('edit_comments'))
			return 'Access denied';
		// Safety check - username/login
		if(!$session->get_permissions('mod_comments')) // allow mods to edit comments
		{
			if(!$session->user_logged_in) _die('AJAX comment save safety check failed because you are not logged in. Sometimes this can happen because you are using a browser that does not send cookies as part of AJAX requests.<br /><br />Please log in and try again.');
			$q = 'SELECT c.name FROM ' . table_prefix.'comments c, ' . table_prefix.'users u WHERE comment_id=' . $id . ' AND page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\' AND u.user_id=c.user_id;';
			$s = $db->sql_query($q);
			if(!$s) _die('SQL error during safety check: '.$db->get_error().'<br /><br />Attempted SQL:<br /><pre>'.htmlspecialchars($q).'</pre>');
			$r = $db->fetchrow($s);
			if($db->numrows() < 1 || $r['name'] != $session->username) _die('Safety check failed, probably due to a hacking attempt.');
			$db->free_result();
		}
		$s = RenderMan::preprocess_text($subject);
		$t = RenderMan::preprocess_text($text);
		$sql  = 'UPDATE ' . table_prefix.'comments SET subject=\'' . $s . '\',comment_data=\'' . $t . '\' WHERE comment_id=' . $id . ' AND page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\'';
		$result = $db->sql_query($sql);
		if($result)
		return 'good';
		else return 'Enano encountered a problem whilst saving the comment.
		Performed SQL:
		' . $sql . '
		
		Error returned by MySQL: '.$db->get_error();
	}
	
	/**
 	* Deletes a comment.
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @param $name the name the user posted under
 	* @param $subj the subject of the comment to be deleted
 	* @param $text the text of the comment to be deleted
 	* @param $id the javascript list ID, used internally by the client-side app
 	* @return string
 	*/
	
	public static function deletecomment($page_id, $namespace, $name, $subj, $text, $id)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		
		if(!$session->get_permissions('edit_comments'))
			return 'alert("Access to delete/edit comments is denied");';
		
		if(!preg_match('#^([0-9]+)$#', (string)$id)) die('$_GET[id] is improperly formed.');
		$n = $db->escape($name);
		$s = $db->escape($subj);
		$t = $db->escape($text);
		
		// Safety check - username/login
		if(!$session->get_permissions('mod_comments')) // allows mods to delete comments
		{
			if(!$session->user_logged_in) _die('AJAX comment save safety check failed because you are not logged in. Sometimes this can happen because you are using a browser that does not send cookies as part of AJAX requests.<br /><br />Please log in and try again.');
			$q = 'SELECT c.name FROM ' . table_prefix.'comments c, ' . table_prefix.'users u WHERE comment_data=\'' . $t . '\' AND subject=\'' . $s . '\' AND page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\' AND u.user_id=c.user_id;';
			$s = $db->sql_query($q);
			if(!$s) _die('SQL error during safety check: '.$db->get_error().'<br /><br />Attempted SQL:<br /><pre>'.htmlspecialchars($q).'</pre>');
			$r = $db->fetchrow($s);
			if($db->numrows() < 1 || $r['name'] != $session->username) _die('Safety check failed, probably due to a hacking attempt.');
			$db->free_result();
		}
		$q = 'DELETE FROM ' . table_prefix.'comments WHERE page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\' AND name=\'' . $n . '\' AND subject=\'' . $s . '\' AND comment_data=\'' . $t . '\' LIMIT 1;';
		$e=$db->sql_query($q);
		if(!$e) return('alert(unesape(\''.rawurlencode('Error during query: '.$db->get_error().'\n\nQuery:\n' . $q) . '\'));');
		return('good');
	}
	
	/**
 	* Deletes a comment in a cleaner fashion.
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @param $id the comment ID (primary key)
 	* @return string
 	*/
	
	public static function deletecomment_neater($page_id, $namespace, $id)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		
		if(!preg_match('#^([0-9]+)$#', (string)$id)) die('$_GET[id] is improperly formed.');
		
		if(!$session->get_permissions('edit_comments'))
			return 'alert("Access to delete/edit comments is denied");';
		
		// Safety check - username/login
		if(!$session->get_permissions('mod_comments')) // allows mods to delete comments
		{
			if(!$session->user_logged_in) _die('AJAX comment save safety check failed because you are not logged in. Sometimes this can happen because you are using a browser that does not send cookies as part of AJAX requests.<br /><br />Please log in and try again.');
			$q = 'SELECT c.name FROM ' . table_prefix.'comments c, ' . table_prefix.'users u WHERE comment_id=' . $id . ' AND page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\' AND u.user_id=c.user_id;';
			$s = $db->sql_query($q);
			if(!$s) _die('SQL error during safety check: '.$db->get_error().'<br /><br />Attempted SQL:<br /><pre>'.htmlspecialchars($q).'</pre>');
			$r = $db->fetchrow($s);
			if($db->numrows() < 1 || $r['name'] != $session->username) _die('Safety check failed, probably due to a hacking attempt.');
			$db->free_result();
		}
		$q = 'DELETE FROM ' . table_prefix.'comments WHERE page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\' AND comment_id=' . $id . ' LIMIT 1;';
		$e=$db->sql_query($q);
		if(!$e) return('alert(unesape(\''.rawurlencode('Error during query: '.$db->get_error().'\n\nQuery:\n' . $q) . '\'));');
		return('good');
	}
	
	/**
 	* Renames a page.
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @param $name the new name for the page
 	* @return string error string or success message
 	*/
 	
	public static function rename($page_id, $namespace, $name)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $lang;
		
		$page = new PageProcessor($page_id, $namespace);
		return $page->rename_page($name);
	}
	
	/**
 	* Flushes (clears) the action logs for a given page
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @return string error/success string
 	*/
 	
	public static function flushlogs($page_id, $namespace)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $lang;
		if ( !is_object($lang) && defined('IN_ENANO_INSTALL') )
		{
			// This is a special exception for the Enano installer, which doesn't init languages yet.
			$lang = new Language('eng');
		}
		if(!$session->get_permissions('clear_logs') && !defined('IN_ENANO_INSTALL'))
		{
			return $lang->get('etc_access_denied');
		}
		if ( !$session->sid_super )
		{
			return $lang->get('etc_access_denied_need_reauth');
		}
		
		$page_id_db = $db->escape($page_id);
		$namespace_db = $db->escape($namespace);
		
		// If we're flushing a file, also clear all revisions before the current
		if ( $namespace == 'File' )
		{
			$q = $db->sql_query('SELECT file_id FROM ' . table_prefix . "files WHERE page_id='$page_id_db' ORDER BY time_id DESC;");
			if ( !$q )
				$db->_die();
			// discard first row (current revision)
			$db->fetchrow();
			$id_list = array();
			while ( $row = $db->fetchrow() )
				$id_list[] = $row['file_id'];
			
			require_once(ENANO_ROOT . '/includes/namespaces/file.php');
			
			// clear out each file
			foreach ( $id_list as $id )
				Namespace_File::delete_file($id);
		}
		
		$q = $db->sql_query('DELETE FROM ' . table_prefix . "logs WHERE page_id='$page_id_db' AND namespace='$namespace';");
		if ( !$q )
			$db->_die('The log entries could not be deleted.');
		
		// If the page exists, make a backup of it in case it gets spammed/vandalized
		// If not, the admin's probably deleting a trash page
		if ( isPage($paths->get_pathskey($page_id, $namespace)) )
		{
			$q = $db->sql_query('SELECT page_text,char_tag FROM ' . table_prefix . "page_text WHERE page_id='$page_id_db' AND namespace='$namespace_db';");
			if ( !$q )
				$db->_die('The current page text could not be selected; as a result, creating the backup of the page failed. Please make a backup copy of the page by clicking Edit this page and then clicking Save Changes.');
			$row = $db->fetchrow();
			$db->free_result();
			$minor_edit = ( ENANO_DBLAYER == 'MYSQL' ) ? 'false' : '0';
			$username = $db->escape($session->username);
			$q = 'INSERT INTO ' . table_prefix . "logs ( log_type, action, time_id, date_string, page_id, namespace, page_text, char_tag, author, author_uid, edit_summary, minor_edit ) VALUES\n"
 				. "  ('page', 'edit', " . time() . ", 'DEPRECATED', '$page_id', '$namespace', '" . $db->escape($row['page_text']) . "', '', '{$username}', $session->user_id, '" . $lang->get('page_flushlogs_backup_summary') . "', $minor_edit);";
			if ( !$db->sql_query($q) )
				$db->_die('The history (log) entry could not be inserted into the logs table.');
		}
		
		return $lang->get('ajax_clearlogs_success');
	}
	
	/**
 	* Deletes a page.
 	* @param string $page_id the condemned page ID
 	* @param string $namespace the condemned namespace
 	* @param string The reason for deleting the page in question
 	* @return string
 	*/
 	
	public static function deletepage($page_id, $namespace, $reason)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $lang;
		global $cache;
		$perms = $session->fetch_page_acl($page_id, $namespace);
		$x = trim($reason);
		if ( empty($x) )
		{
			return $lang->get('ajax_delete_need_reason');
		}
		if(!$perms->get_permissions('delete_page')) return('Administrative privileges are required to delete pages, you loser.');
		
		if ( !$session->sid_super )
		{
			return $lang->get('etc_access_denied_need_reauth');
		}
		
		$e = $db->sql_query('INSERT INTO ' . table_prefix.'logs(time_id,date_string,log_type,action,page_id,namespace,author,author_uid,edit_summary) VALUES('.time().', \''.enano_date(ED_DATE | ED_TIME).'\', \'page\', \'delete\', \'' . $page_id . '\', \'' . $namespace . '\', \'' . $session->username . '\', ' . $session->user_id . ', \'' . $db->escape(htmlspecialchars($reason)) . '\')');
		if(!$e) $db->_die('The page log entry could not be inserted.');
		$e = $db->sql_query('DELETE FROM ' . table_prefix.'categories WHERE page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\'');
		if(!$e) $db->_die('The page categorization entries could not be deleted.');
		$e = $db->sql_query('DELETE FROM ' . table_prefix.'comments WHERE page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\'');
		if(!$e) $db->_die('The page comments could not be deleted.');
		$e = $db->sql_query('DELETE FROM ' . table_prefix.'page_text WHERE page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\'');
		if(!$e) $db->_die('The page text entry could not be deleted.');
		$e = $db->sql_query('DELETE FROM ' . table_prefix.'pages WHERE urlname=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\'');
		if(!$e) $db->_die('The page entry could not be deleted.');
		if ( $namespace == 'File' )
		{
			$e = $db->sql_query('DELETE FROM ' . table_prefix.'files WHERE page_id=\'' . $page_id . '\'');
			if(!$e) $db->_die('The file entry could not be deleted.');
		}
		$cache->purge('page_meta');
		return $lang->get('ajax_delete_success');
	}
	
	/**
 	* Deletes files associated with a File page.
 	* @param string Page ID
 	*/
	
	public static function delete_page_files($page_id)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		
		$q = $db->sql_query('SELECT file_id, filename, file_key, time_id, file_extension FROM ' . table_prefix . "files WHERE page_id = '{$db->escape($page_id)}';");
		if ( !$q )
			$db->_die();
		
		while ( $row = $db->fetchrow() )
		{
			// wipe original file
			foreach ( array(
					ENANO_ROOT . "/files/{$row['file_key']}_{$row['time_id']}{$row['file_extension']}",
					ENANO_ROOT . "/files/{$row['file_key']}{$row['file_extension']}"
				) as $orig_file )
			{
				if ( file_exists($orig_file) )
					@unlink($orig_file);
			}
			
			// wipe cached files
			if ( $dr = @opendir(ENANO_ROOT . '/cache/') )
			{
				// lol404.jpg-1217958283-200x320.jpg
				while ( $dh = @readdir($dr) )
				{
					$regexp = ':^' . preg_quote("{$row['filename']}-{$row['time_id']}-") . '[0-9]+x[0-9]+\.' . ltrim($row['file_extension'], '.') . '$:';
					if ( preg_match($regexp, $dh) )
					{
						@unlink(ENANO_ROOT . "/cache/$dh");
					}
				}
				@closedir($dr);
			}
		}
		
		$q = $db->sql_query('DELETE FROM ' . table_prefix . "files WHERE page_id = '{$db->escape($page_id)}';");
		if ( !$q )
			$db->die();
		
		return true;
	}
	
	/**
 	* Increments the deletion votes for a page by 1, and adds the current username/IP to the list of users that have voted for the page to prevent dual-voting
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @return string
 	*/
 	
	public static function delvote($page_id, $namespace)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $lang;
		global $cache;
		
		if ( !$session->get_permissions('vote_delete') )
		{
			return $lang->get('etc_access_denied');
		}
		
		if ( $namespace == 'Admin' || $namespace == 'Special' || $namespace == 'System' )
		{
			return 'Special pages and system messages can\'t be voted for deletion.';
		}
		
		$pname = $paths->nslist[$namespace] . sanitize_page_id($page_id);
		
		if ( !isPage($pname) )
		{
			return 'The page does not exist.';
		}
		
		$ns = namespace_factory($page_id, $namespace);
		$cdata = $ns->get_cdata();
		
		$cv  =& $cdata['delvotes'];
		$ips =& $cdata['delvote_ips'];
		
		if ( empty($ips) )
		{
			$ips = array(
				'ip' => array(),
				'u' => array()
				);
		}
		else
		{
			$ips = @unserialize($ips);
			if ( !$ips )
			{
				$ips = array(
				'ip' => array(),
				'u' => array()
				);
			}
		}
		
		if ( in_array($session->username, $ips['u']) || in_array($_SERVER['REMOTE_ADDR'], $ips['ip']) )
		{
			return $lang->get('ajax_delvote_already_voted');
		}
		
		$ips['u'][] = $session->username;
		$ips['ip'][] = $_SERVER['REMOTE_ADDR'];
		$ips = $db->escape( serialize($ips) );
		
		$cv++;
		
		$q = 'UPDATE ' . table_prefix.'pages SET delvotes=' . $cv . ',delvote_ips=\'' . $ips . '\' WHERE urlname=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\'';
		$w = $db->sql_query($q);
		if ( !$w )
			$db->_die();
		
		// all done, flush page cache to mark it up
		$cache->purge('page_meta');
		
		return $lang->get('ajax_delvote_success');
	}
	
	/**
 	* Resets the number of votes against a page to 0.
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @return string
 	*/
	
	public static function resetdelvotes($page_id, $namespace)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $lang;
		global $cache;
		
		if ( !$session->get_permissions('vote_reset') )
		{
			return $lang->get('etc_access_denied');
		}
		
		$page_id = $db->escape($page_id);
		$namespace = $db->escape($namespace);
		
		// pull existing info
		$q = $db->sql_query('SELECT delvotes, delvote_ips FROM ' . table_prefix . "pages WHERE urlname = '$page_id' AND namespace = '$namespace'");
		if ( !$q )
			$db->_die();
		if ( $db->numrows() < 1 )
			return $lang->get('page_err_page_not_exist');
		
		list($delvotes, $delvote_ips) = $db->fetchrow_num();
		$db->free_result();
		$delvote_ips = $db->escape($delvote_ips);
		$username = $db->escape($session->username);
		
		// log action
		$time = time();
		$q = $db->sql_query('INSERT INTO ' . table_prefix . "logs (time_id, log_type, action, edit_summary, page_text, author, author_uid, page_id, namespace) VALUES\n"
											. "  ( $time, 'page', 'votereset', '$delvotes', '$delvote_ips', '$username', $session->user_id, '$page_id', '$namespace' )");
		if ( !$q )
			$db->_die();
		
		// reset votes
		$empty_vote_record = $db->escape(serialize(array('ip'=>array(),'u'=>array())));
		$q = 'UPDATE ' . table_prefix.'pages SET delvotes=0,delvote_ips=\'' . $empty_vote_record . '\' WHERE urlname=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\'';
		$e = $db->sql_query($q);
		if ( !$e )
		{
			$db->_die('The number of delete votes was not reset.');
		}
		else
		{
			$cache->purge('page_meta');
			return $lang->get('ajax_delvote_reset_success');
		}
	}
	
	/**
 	* Gets a list of styles for a given theme name. As of Banshee, this returns JSON.
 	* @param $id the name of the directory for the theme
 	* @return string JSON string with an array containing a list of themes
 	*/
 	
	public static function getstyles()
	{
		
		if ( !preg_match('/^([a-z0-9_-]+)$/', $_GET['id']) )
			return enano_json_encode(false);
		
		$dir = './themes/' . $_GET['id'] . '/css/';
		$list = Array();
		// Open a known directory, and proceed to read its contents
		if (is_dir($dir)) {
			if ($dh = opendir($dir)) {
				while (($file = readdir($dh)) !== false) {
					if ( preg_match('#^(.*?)\.css$#is', $file) && $file != '_printable.css' ) // _printable.css should be included with every theme
					{                                                                         // it should be a copy of the original style, but
																																										// mostly black and white
																																										// Note to self: document this
						$list[] = substr($file, 0, strlen($file)-4);
					}
				}
				closedir($dh);
			}
		}
		else
		{
			return(enano_json_encode(Array('mode' => 'error', 'error' => $dir.' is not a dir')));
		}
		
		return enano_json_encode($list);
	}
	
	/**
 	* Assembles a Javascript app with category information
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @return string Javascript code
 	*/
 	
	public static function catedit($page_id, $namespace)
	{
		list($js, $html) = PageUtils::catedit_raw($page_id, $namespace);
		return $js . ' /* BEGIN CONTENT */ document.getElementById("ajaxEditContainer").innerHTML = unescape(\''.rawurlencode($html).'\');';
	}
	
	/**
 	* Does the actual HTML/javascript generation for cat editing, but returns an array
 	* @access private
 	*/
 	
	public static function catedit_raw($page_id, $namespace)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $lang;
		
		// notes
		// span class is catCheck
		// return array(jsblob, innerHTML)
		/*
		$perms = $session->fetch_page_acl($cat_info[$i]['urlname_nons'], 'Category');
		$cat_is_protected = ( !$session->get_permissions('edit_cat') || !$perms->get_permissions('edit_cat') ||
 				( $cat_info[$i]['really_protected'] && !$perms->get_permissions('even_when_protected') ) )
		*/
		
		// two buffers: one is HTML and one is Javascript.
		$js = $html = '';
		
		// page permissions
		$page_perms = $session->fetch_page_acl($page_id, $namespace);
		
		// Pull the list of categories this page is in
		$cats_member_of = array();
		$q = $db->sql_query('SELECT category_id FROM ' . table_prefix . 'categories WHERE page_id = \'' . $db->escape($page_id) . '\' AND namespace = \'' . $db->escape($namespace) . '\';');
		if ( !$q )
			$db->_die();
		while ( $row = $db->fetchrow() )
		{
			$cats_member_of[] = $row['category_id'];
		}
		
		// Get a list of all categories on the site
		$q = $db->sql_query('SELECT * FROM ' . table_prefix . 'pages WHERE namespace = \'Category\' ORDER BY name ASC;');
		if ( !$q )
			$db->_die();
		
		$categories = array();
		while ( $row = $db->fetchrow($q) )
		{
			// bake page information
			$row = Namespace_Default::bake_cdata($row);
			// add our own info
			$row['checked'] = in_array($row['urlname_nons'], $cats_member_of);
			$row['exists'] = true;
			$row['perms'] = $session->fetch_page_acl($row['urlname_nons'], 'Category');
			$row['disabled'] = (
					// no permissions to edit categorization in this category, or
					!$row['perms']->get_permissions('edit_cat') ||
					// category is protected, and no protect override permissions
					( $row['really_protected'] && !$row['perms']->get_permissions('even_when_protected') )
				);
			// append to array
			$categories[ $row['urlname_nons'] ] = $row;
		}
		
		// fabricate information on categories that don't exist.
		foreach ( $cats_member_of as $category )
		{
			if ( isset($categories[$category]) )
				// already have it in the array, skip
				continue;
			// create page metadata
			$row = Namespace_Default::bake_cdata(array(
					'urlname' => $category,
					'namespace' => 'Category'
				));
			// we know it's in this category
			$row['checked'] = true;
			// we know it doesn't exist
			$row['exists'] = false;
			$row['perms'] = $session->fetch_page_acl($category, 'Category');
			$row['disabled'] = (
					// no permissions to edit categorization in this category (honor inheritance and everything)
					!$row['perms']->get_permissions('edit_cat')
					// not checking protection because it's defaulted to off
				);
			// append
			$categories[ $category ] = $row;
		}
		
		// spit out the form
		$html .= '<h3>' . $lang->get('catedit_title') . '</h3>';
		$html .= '<form name="mdgCatForm" action="'.makeUrlNS($namespace, $page_id, 'do=catedit').'" method="post" enctype="multipart/form-data">';
		foreach ( $categories as $category )
		{
			$html .= '<span class="catCheck"><label>';
			if ( !$category['exists'] )
				$html .= '<del>';
			$html .= '<input type="checkbox" name="categories[]" value="' . htmlspecialchars($category['urlname_nons']) . '" ';
			if ( $category['checked'] )
				$html .= 'checked="checked" ';
			if ( $category['disabled'] )
				$html .= 'disabled="disabled" ';
			$html .= ' />' . htmlspecialchars($category['name']);
			// lock icon
			if ( $category['protected'] > 0 )
				$html .= ' <img alt="' . $lang->get('catedit_msg_protected_tip') . '" width="16" height="16" src="' . cdnPath . '/images/lock16.png" />';
			if ( !$category['exists'] )
				$html .= '</del>';
			$html .= '</label></span><br />';
		}
		if ( count($categories) < 1 )
			$html .= '<p>' . $lang->get('catedit_no_categories') . '</p>';
		// submit buttons
		$save_disabled = ( count($categories) < 1 ) ? 'disabled="disabled"' : '';
		$html .= '<div style="border-top: 1px solid #CCC; padding-top: 5px; margin-top: 10px;">
					<input name="save" ' . $save_disabled . ' style="font-weight: bold;" type="submit" onclick="ajaxCatSave(); return false;" value="' . $lang->get('etc_save_changes') . '" />
					<input name="cancel" type="submit" onclick="ajaxReset(); return false;" value="' . $lang->get('etc_cancel') . '" />
				  </div>';
		
		$html .= '</form>';
		
		return array($js, $html);
	}
	
	/**
 	* Saves category information
 	* WARNING: If $which_cats is empty, all the category information for the selected page will be nuked!
 	* @param $page_id string the page ID
 	* @param $namespace string the namespace
 	* @param $which_cats array associative array of categories to put the page in
 	* @return string "GOOD" on success, error string on failure
 	*/
	
	public static function catsave($page_id, $namespace, $which_cats)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		if(!$session->get_permissions('edit_cat')) return('Insufficient privileges to change category information');
		
		$page_perms = $session->fetch_page_acl($page_id, $namespace);
		$ns = namespace_factory($page_id, $namespace);
		$page_data = $ns->get_cdata();
		if ( !$page_perms->get_permissions('edit_cat') ||
				( $page_data['really_protected'] && !$page_perms->get_permissions('even_when_protected') ) )
			return 'Insufficient privileges';
		
		// Pull the list of categories this page is in
		$cats_member_of = array();
		$q = $db->sql_query('SELECT category_id FROM ' . table_prefix . 'categories WHERE page_id = \'' . $db->escape($page_id) . '\' AND namespace = \'' . $db->escape($namespace) . '\';');
		if ( !$q )
			$db->_die();
		while ( $row = $db->fetchrow() )
		{
			$cats_member_of[] = $row['category_id'];
		}
		
		// Get a list of all categories on the site
		$q = $db->sql_query('SELECT * FROM ' . table_prefix . 'pages WHERE namespace = \'Category\' ORDER BY name ASC;');
		if ( !$q )
			$db->_die();
		
		$categories = array();
		while ( $row = $db->fetchrow($q) )
		{
			// bake page information
			$row = Namespace_Default::bake_cdata($row);
			// add our own info
			$row['checked'] = in_array($row['urlname_nons'], $cats_member_of);
			$row['exists'] = true;
			$row['perms'] = $session->fetch_page_acl($row['urlname_nons'], 'Category');
			$row['disabled'] = (
					// no permissions to edit categorization in this category, or
					!$row['perms']->get_permissions('edit_cat') ||
					// category is protected, and no protect override permissions
					( $row['really_protected'] && !$row['perms']->get_permissions('even_when_protected') )
				);
			// append to array
			$categories[ $row['urlname_nons'] ] = $row;
		}
		
		// fabricate information on categories that don't exist.
		foreach ( $cats_member_of as $category )
		{
			if ( isset($categories[$category]) )
				// already have it in the array, skip
				continue;
			// create page metadata
			$row = Namespace_Default::bake_cdata(array(
					'urlname' => $category,
					'namespace' => 'Category'
				));
			// we know it's in this category
			$row['checked'] = true;
			// we know it doesn't exist
			$row['exists'] = false;
			$row['perms'] = $session->fetch_page_acl($category, 'Category');
			$row['disabled'] = (
					// no permissions to edit categorization in this category (honor inheritance and everything)
					!$row['perms']->get_permissions('edit_cat')
					// not checking protection because it's defaulted to off, and we know we are using the defaults
					// because we made it past the check above ;)
				);
			// append
			$categories[ $category ] = $row;
		}
		
		$to_insert = $to_delete = array();
		// go through categories and mark needed changes
		foreach ( $categories as $cat_id => $category )
		{
			// allowed to change it?
			if ( $category['disabled'] )
				continue;
			
			if ( $category['checked'] && !in_array($cat_id, $which_cats) )
			{
				// delete
				$to_delete[] = $cat_id;
			}
			else if ( !$category['checked'] && in_array($cat_id, $which_cats) )
			{
				// insert
				$to_insert[] = $cat_id;
			}
			else
			{
				// no change
			}
		}
		
		// commit changes
		if ( !empty($to_insert) )
		{
			$rows = array();
			foreach ( $to_insert as $cat_id )
			{
				$rows[] = "('{$db->escape($page_id)}', '{$db->escape($namespace)}', '{$db->escape($cat_id)}')";
			}
			$q = $db->sql_query("INSERT INTO " . table_prefix . "categories(page_id, namespace, category_id) VALUES\n  "
									. implode(",\n  ", $rows) . ";");
			if ( !$q )
				$db->_die();
		}
		if ( !empty($to_delete) )
		{
			$entries = array();
			foreach ( $to_delete as $cat_id )
			{
				$entries[] = "category_id = '{$db->escape($cat_id)}'";
			}
			$q = $db->sql_query("DELETE FROM " . table_prefix . "categories WHERE page_id = '{$db->escape($page_id)}' AND namespace = '{$db->escape($namespace)}'\n"
									. "  AND ( " . implode(' OR ', $entries) . " );");
			if ( !$q )
				$db->_die();
		}
		
		
		return 'GOOD';
	}
	
	/**
 	* Sets the wiki mode level for a page.
 	* @param $page_id string the page ID
 	* @param $namespace string the namespace
 	* @param $level int 0 for off, 1 for on, 2 for use global setting
 	* @return string "GOOD" on success, error string on failure
 	*/
	
	public static function setwikimode($page_id, $namespace, $level)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $cache;
		
		if(!$session->get_permissions('set_wiki_mode')) return('Insufficient access rights');
		if ( !isset($level) || ( isset($level) && !preg_match('#^([0-2]){1}$#', (string)$level) ) )
		{
			return('Invalid mode string');
		}
		$q = $db->sql_query('UPDATE ' . table_prefix.'pages SET wiki_mode=' . $level . ' WHERE urlname=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\';');
		if ( !$q )
		{
			return('Error during update query: '.$db->get_error()."\n\nSQL Backtrace:\n".$db->sql_backtrace());
		}
		
		$cache->purge('page_meta');
		return('GOOD');
	}
	
	/**
 	* Sets the access password for a page.
 	* @param $page_id string the page ID
 	* @param $namespace string the namespace
 	* @param $pass string the SHA1 hash of the password - if the password doesn't match the regex ^([0-9a-f]*){40,40}$ it will be sha1'ed
 	* @return string
 	*/
	
	public static function setpass($page_id, $namespace, $pass)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $lang, $cache;
		// Determine permissions
		$ns = namespace_factory($page_id, $namespace);
		$cdata = $ns->get_cdata();
		if ( $cdata['password'] != '' )
			$a = $session->get_permissions('password_reset');
		else
			$a = $session->get_permissions('password_set');
		if ( !$a )
			return $lang->get('etc_access_denied');
		if ( !isset($pass) )
			return('Password was not set on URL');
		$p = $pass;
		if ( !preg_match('#([0-9a-f]){40,40}#', $p) )
		{
			$p = sha1($p);
		}
		if ( $p == 'da39a3ee5e6b4b0d3255bfef95601890afd80709' )
			// sha1('') = da39a3ee5e6b4b0d3255bfef95601890afd80709
			$p = '';
		$e = $db->sql_query('UPDATE ' . table_prefix.'pages SET password=\'' . $p . '\' WHERE urlname=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\';');
		if ( !$e )
		{
			die('PageUtils::setpass(): Error during update query: '.$db->get_error()."\n\nSQL Backtrace:\n".$db->sql_backtrace());
		}
		// Is the new password blank?
		if ( $p == '' )
		{
			return $lang->get('ajax_password_disable_success');
		}
		else
		{
			return $lang->get('ajax_password_success');
		}
	}
	
	/**
 	* Generates some preview HTML
 	* @param $text string the wikitext to use
 	* @return string
 	*/
 	
	public static function genPreview($text)
	{
		global $lang;
		$ret = '<div class="info-box">' . $lang->get('editor_preview_blurb') . '</div><div style="background-color: #F8F8F8; padding: 10px; border: 1px dashed #406080; max-height: 250px; overflow: auto; margin: 10px 0;">';
		$text = RenderMan::render(RenderMan::preprocess_text($text, false, false));
		ob_start();
		eval('?>' . $text);
		$text = ob_get_contents();
		ob_end_clean();
		$ret .= $text;
		$ret .= '</div>';
		return $ret;
	}
	
	/**
 	* Makes a scrollable box
 	* @param string $text the inner HTML
 	* @param int $height Optional - the maximum height. Defaults to 250.
 	* @return string
 	*/
 	
	public static function scrollBox($text, $height = 250)
	{
		return '<div style="background-color: #F8F8F8; padding: 10px; border: 1px dashed #406080; max-height: '.(string)intval($height).'px; overflow: auto; margin: 1em 0 1em 1em;">' . $text . '</div>';
	}
	
	/**
 	* Generates a diff summary between two page revisions.
 	* @param $page_id the page ID
 	* @param $namespace the namespace
 	* @param $id1 the time ID of the first revision
 	* @param $id2 the time ID of the second revision
 	* @return string XHTML-formatted diff
 	*/
 	
	public static function pagediff($page_id, $namespace, $id1, $id2)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $lang;
		
		if ( !$session->get_permissions('history_view') )
			return $lang->get('etc_access_denied');
		
		if(!preg_match('#^([0-9]+)$#', (string)$id1) ||
 			!preg_match('#^([0-9]+)$#', (string)$id2  )) return 'SQL injection attempt';
		// OK we made it through security
		// Safest way to make sure we don't end up with the revisions in wrong columns is to make 2 queries
		if ( !$q1 = $db->sql_query('SELECT time_id,page_text,char_tag,author,edit_summary FROM ' . table_prefix.'logs WHERE log_id = ' . $id1 . ' AND log_type=\'page\' AND action=\'edit\' AND page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\';')) return 'MySQL error: ' . $db->get_error();
		if ( !$q2 = $db->sql_query('SELECT time_id,page_text,char_tag,author,edit_summary FROM ' . table_prefix.'logs WHERE log_id = ' . $id2 . ' AND log_type=\'page\' AND action=\'edit\' AND page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\';')) return 'MySQL error: ' . $db->get_error();
		$row1 = $db->fetchrow($q1);
		$db->free_result($q1);
		$row2 = $db->fetchrow($q2);
		$db->free_result($q2);
		if(sizeof($row1) < 1 || sizeof($row2) < 2)
		{
			if ( !$q1 = $db->sql_query('SELECT time_id,page_text,char_tag,author,edit_summary FROM ' . table_prefix.'logs WHERE time_id = ' . $id1 . ' AND log_type=\'page\' AND action=\'edit\' AND page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\';')) return 'MySQL error: ' . $db->get_error();
			if ( !$q2 = $db->sql_query('SELECT time_id,page_text,char_tag,author,edit_summary FROM ' . table_prefix.'logs WHERE time_id = ' . $id2 . ' AND log_type=\'page\' AND action=\'edit\' AND page_id=\'' . $page_id . '\' AND namespace=\'' . $namespace . '\';')) return 'MySQL error: ' . $db->get_error();
			$row1 = $db->fetchrow($q1);
			$db->free_result($q1);
			$row2 = $db->fetchrow($q2);
			$db->free_result($q2);
			if(sizeof($row1) < 1 || sizeof($row2) < 2)
				return 'Couldn\'t find any rows that matched the query. The time ID probably doesn\'t exist in the logs table.';
		}
		$text1 = $row1['page_text'];
		$text2 = $row2['page_text'];
		$time1 = enano_date(ED_DATE | ED_TIME, $row1['time_id']);
		$time2 = enano_date(ED_DATE | ED_TIME, $row2['time_id']);
		$_ob = "
		<p>" . $lang->get('history_lbl_comparingrevisions') . " {$time1} &rarr; {$time2}</p>
		";
		// Free some memory
		unset($row1, $row2, $q1, $q2);
		
		$_ob .= RenderMan::diff($text1, $text2);
		return $_ob;
	}
	
	/**
 	* Gets ACL information about the selected page for target type X and target ID Y.
 	* @param array $parms What to select. This is an array purely for JSON compatibility. It should be an associative array with keys target_type and target_id.
 	* @return array
 	*/
 	
	public static function acl_editor($parms = Array())
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $lang;
		
		if(!$session->get_permissions('edit_acl') && ( $session->user_level < USER_LEVEL_ADMIN || !defined('ACL_ALWAYS_ALLOW_ADMIN_EDIT_ACL')) )
		{
			return Array(
				'mode' => 'error',
				'error' => $lang->get('acl_err_access_denied')
				);
		}
		if ( !$session->sid_super )
		{
			return Array(
				'mode' => 'error',
				'error' => $lang->get('etc_access_denied_need_reauth')
				);
		}
		$parms['page_id'] = ( isset($parms['page_id']) ) ? $parms['page_id'] : false;
		$parms['namespace'] = ( isset($parms['namespace']) ) ? $parms['namespace'] : false;
		$page_id =& $parms['page_id'];
		$namespace =& $parms['namespace'];
		$page_where_clause      = ( empty($page_id) || empty($namespace) ) ? 'AND a.page_id IS NULL AND a.namespace IS NULL' : 'AND a.page_id=\'' . $db->escape($page_id) . '\' AND a.namespace=\'' . $db->escape($namespace) . '\'';
		$page_where_clause_lite = ( empty($page_id) || empty($namespace) ) ? 'AND page_id IS NULL AND namespace IS NULL' : 'AND page_id=\'' . $db->escape($page_id) . '\' AND namespace=\'' . $db->escape($namespace) . '\'';
		//die(print_r($page_id,true));
		$template->load_theme();
		// $perms_obj = $session->fetch_page_acl($page_id, $namespace);
		$perms_obj =& $session;
		$return = Array();
		if ( !file_exists(ENANO_ROOT . '/themes/' . $session->theme . '/acledit.tpl') )
		{
			return Array(
				'mode' => 'error',
				'error' => $lang->get('acl_err_missing_template'),
			);
		}
		$return['template'] = $template->extract_vars('acledit.tpl');
		$return['page_id'] = $page_id;
		$return['namespace'] = $namespace;
		if(isset($parms['mode']))
		{
			switch($parms['mode'])
			{
				case 'listgroups':
					$return['groups'] = Array();
					$q = $db->sql_query('SELECT group_id,group_name FROM ' . table_prefix.'groups ORDER BY group_name ASC;');
					while($row = $db->fetchrow())
					{
						$return['groups'][] = Array(
							'id' => $row['group_id'],
							'name' => $row['group_name'],
							);
					}
					$db->free_result();
					$return['page_groups'] = Array();
					$q = $db->sql_query('SELECT pg_id,pg_name FROM ' . table_prefix.'page_groups ORDER BY pg_name ASC;');
					if ( !$q )
						return Array(
							'mode' => 'error',
							'error' => $db->get_error()
							);
					while ( $row = $db->fetchrow() )
					{
						$return['page_groups'][] = Array(
								'id' => $row['pg_id'],
								'name' => $row['pg_name']
							);
					}
					break;
				case 'seltarget_id':
					if ( !is_int($parms['target_id']) )
					{
						return Array(
							'mode' => 'error',
							'error' => 'Expected parameter target_id type int'
							);
					}
					$q = $db->sql_query('SELECT target_id, target_type, page_id, namespace, rules FROM ' . table_prefix . "acl WHERE rule_id = {$parms['target_id']};");
					if ( !$q )
						return Array(
							'mode' => 'error',
							'error' => $db->get_error()
							);
					if ( $db->numrows() < 1 )
						return Array(
							'mode' => 'error',
							'error' => "No rule with ID {$parms['target_id']} found"
							);
						$parms = $db->fetchrow();
						$db->free_result();
						
						// regenerate page selection
						$parms['page_id'] = ( isset($parms['page_id']) ) ? $parms['page_id'] : false;
						$parms['namespace'] = ( isset($parms['namespace']) ) ? $parms['namespace'] : false;
						$parms['mode'] = 'seltarget_id';
						$page_id =& $parms['page_id'];
						$namespace =& $parms['namespace'];
						$page_where_clause      = ( empty($page_id) || empty($namespace) ) ? 'AND a.page_id IS NULL AND a.namespace IS NULL' : 'AND a.page_id=\'' . $db->escape($page_id) . '\' AND a.namespace=\'' . $db->escape($namespace) . '\'';
						$page_where_clause_lite = ( empty($page_id) || empty($namespace) ) ? 'AND page_id IS NULL AND namespace IS NULL' : 'AND page_id=\'' . $db->escape($page_id) . '\' AND namespace=\'' . $db->escape($namespace) . '\'';
						
						$return['page_id'] = $parms['page_id'];
						$return['namespace'] = $parms['namespace'];
						
						// From here, let the seltarget handler take over
				case 'seltarget':
					$return['mode'] = 'seltarget';
					$return['acl_types'] = $perms_obj->acl_types;
					$return['acl_deps'] = $perms_obj->acl_deps;
					$return['acl_descs'] = $perms_obj->acl_descs;
					$return['target_type'] = $parms['target_type'];
					$return['target_id'] = $parms['target_id'];
					switch($parms['target_type'])
					{
						case ACL_TYPE_USER:
							$user_col = ( $parms['mode'] == 'seltarget_id' ) ? 'user_id' : 'username';
							$q = $db->sql_query('SELECT a.rules,u.user_id,u.username FROM ' . table_prefix.'users AS u
									LEFT JOIN ' . table_prefix.'acl AS a
										ON a.target_id=u.user_id
									WHERE a.target_type='.ACL_TYPE_USER.'
										AND u.' . $user_col . ' = \'' . $db->escape($parms['target_id']) . '\'
										' . $page_where_clause . ';');
							if(!$q)
								return(Array('mode'=>'error','error'=>$db->get_error()));
							if($db->numrows() < 1)
							{
								$return['type'] = 'new';
								$q = $db->sql_query('SELECT user_id,username FROM ' . table_prefix.'users WHERE username=\'' . $db->escape($parms['target_id']) . '\';');
								if(!$q)
									return(Array('mode'=>'error','error'=>$db->get_error()));
								if($db->numrows() < 1)
									return Array('mode'=>'error','error'=>$lang->get('acl_err_user_not_found'),'debug' => $db->sql_backtrace());
								$row = $db->fetchrow();
								$return['target_name'] = $row['username'];
								$return['target_id'] = intval($row['user_id']);
								$return['current_perms'] = array();
							}
							else
							{
								$return['type'] = 'edit';
								$row = $db->fetchrow();
								$return['target_name'] = $row['username'];
								$return['target_id'] = intval($row['user_id']);
								$return['current_perms'] = $session->string_to_perm($row['rules']);
							}
							$db->free_result();
							// Eliminate types that don't apply to this namespace
							if ( $namespace && $namespace != '__PageGroup' )
							{
								foreach ( $return['current_perms'] AS $i => $perm )
								{
									if ( ( $page_id != null && $namespace != null ) && ( !in_array ( $namespace, $session->acl_scope[$i] ) && !in_array('All', $session->acl_scope[$i]) ) )
									{
										// echo "// SCOPE CONTROL: eliminating: $i\n";
										unset($return['current_perms'][$i]);
										unset($return['acl_types'][$i]);
										unset($return['acl_descs'][$i]);
										unset($return['acl_deps'][$i]);
									}
								}
							}
							break;
						case ACL_TYPE_GROUP:
							$q = $db->sql_query('SELECT a.rules,g.group_name,g.group_id FROM ' . table_prefix.'groups AS g
									LEFT JOIN ' . table_prefix.'acl AS a
										ON a.target_id=g.group_id
									WHERE a.target_type='.ACL_TYPE_GROUP.'
										AND g.group_id=\''.intval($parms['target_id']).'\'
										' . $page_where_clause . ';');
							if(!$q)
								return(Array('mode'=>'error','error'=>$db->get_error()));
							if($db->numrows() < 1)
							{
								$return['type'] = 'new';
								$q = $db->sql_query('SELECT group_id,group_name FROM ' . table_prefix.'groups WHERE group_id=\''.intval($parms['target_id']).'\';');
								if(!$q)
									return(Array('mode'=>'error','error'=>$db->get_error()));
								if($db->numrows() < 1)
									return Array('mode'=>'error','error'=>$lang->get('acl_err_bad_group_id'));
								$row = $db->fetchrow();
								$return['target_name'] = $row['group_name'];
								$return['target_id'] = intval($row['group_id']);
								$return['current_perms'] = array();
							}
							else
							{
								$return['type'] = 'edit';
								$row = $db->fetchrow();
								$return['target_name'] = $row['group_name'];
								$return['target_id'] = intval($row['group_id']);
								$return['current_perms'] = $session->string_to_perm($row['rules']);
							}
							$db->free_result();
							// Eliminate types that don't apply to this namespace
							if ( $namespace && $namespace != '__PageGroup' )
							{
								foreach ( $return['current_perms'] AS $i => $perm )
								{
									if ( ( $page_id != false && $namespace != false ) && ( !in_array ( $namespace, $session->acl_scope[$i] ) && !in_array('All', $session->acl_scope[$i]) ) )
									{
										// echo "// SCOPE CONTROL: eliminating: $i\n"; //; ".print_r($namespace,true).":".print_r($page_id,true)."\n";
										unset($return['current_perms'][$i]);
										unset($return['acl_types'][$i]);
										unset($return['acl_descs'][$i]);
										unset($return['acl_deps'][$i]);
									}
								}
							}
							//return Array('mode'=>'debug','text'=>print_r($return, true));
							break;
						default:
							return Array('mode'=>'error','error','Invalid ACL type ID');
							break;
					}
					return $return;
					break;
				case 'save_new':
				case 'save_edit':
					if ( defined('ENANO_DEMO_MODE') )
					{
						return Array('mode'=>'error','error'=>$lang->get('acl_err_demo'));
					}
					$q = $db->sql_query('DELETE FROM ' . table_prefix.'acl WHERE target_type='.intval($parms['target_type']).' AND target_id='.intval($parms['target_id']).'
						' . $page_where_clause_lite . ';');
					if(!$q)
						return Array('mode'=>'error','error'=>$db->get_error());
					if ( sizeof ( $parms['perms'] ) < 1 )
					{
						// As of 1.1.x, this returns success because the rule length is zero if the user selected "inherit" in all columns
						return Array(
							'mode' => 'success',
							'target_type' => $parms['target_type'],
							'target_id' => $parms['target_id'],
							'target_name' => $parms['target_name'],
							'page_id' => $page_id,
							'namespace' => $namespace,
						);
					}
					$rules = $session->perm_to_string($parms['perms']);
					$q = ($page_id && $namespace) ? 'INSERT INTO ' . table_prefix.'acl ( target_type, target_id, page_id, namespace, rules )
 																						VALUES( '.intval($parms['target_type']).', '.intval($parms['target_id']).', \'' . $db->escape($page_id) . '\', \'' . $db->escape($namespace) . '\', \'' . $db->escape($rules) . '\' )' :
																					'INSERT INTO ' . table_prefix.'acl ( target_type, target_id, rules )
 																						VALUES( '.intval($parms['target_type']).', '.intval($parms['target_id']).', \'' . $db->escape($rules) . '\' )';
					if(!$db->sql_query($q)) return Array('mode'=>'error','error'=>$db->get_error());
					return Array(
							'mode' => 'success',
							'target_type' => $parms['target_type'],
							'target_id' => $parms['target_id'],
							'target_name' => $parms['target_name'],
							'page_id' => $page_id,
							'namespace' => $namespace,
						);
					break;
				case 'delete':
					if ( defined('ENANO_DEMO_MODE') )
					{
						return Array('mode'=>'error','error'=>$lang->get('acl_err_demo'));
					}
					$sql = 'DELETE FROM ' . table_prefix.'acl WHERE target_type='.intval($parms['target_type']).' AND target_id='.intval($parms['target_id']).'
						' . $page_where_clause_lite . ';';
					$q = $db->sql_query($sql);
					if(!$q)
						return Array('mode'=>'error','error'=>$db->get_error());
					return Array(
							'mode' => 'delete',
							'target_type' => $parms['target_type'],
							'target_id' => $parms['target_id'],
							'target_name' => $parms['target_name'],
							'page_id' => $page_id,
							'namespace' => $namespace,
						);
					break;
				case 'list_existing':
					
					$return = array(
							'mode'  => 'list_existing',
							'key'   => acl_list_draw_key(),
							'rules' => array()
						);
					
					$acl_columns = 'a.' . implode(', a.', $db->columns_in(table_prefix . 'acl'));
					$users_columns = 'u.' . implode(', u.', $db->columns_in(table_prefix . 'users'));
					$groups_columns = 'g.' . implode(', g.', $db->columns_in(table_prefix . 'groups'));
					$pg_columns = 'p.' . implode(', p.', array('pg_id', 'pg_type', 'pg_name', 'pg_target'));
					
					$q = $db->sql_query("SELECT a.rule_id, u.username, g.group_name, a.target_type, a.target_id, a.page_id, a.namespace, a.rules, p.pg_name\n"
									. "  FROM " . table_prefix . "acl AS a\n"
									. "  LEFT JOIN " . table_prefix . "users AS u\n"
									. "    ON ( (a.target_type = " . ACL_TYPE_USER . " AND a.target_id = u.user_id) OR (u.user_id IS NULL) )\n"
									. "  LEFT JOIN " . table_prefix . "groups AS g\n"
									. "    ON ( (a.target_type = " . ACL_TYPE_GROUP . " AND a.target_id = g.group_id) OR (g.group_id IS NULL) )\n"
									. "  LEFT JOIN " . table_prefix . "page_groups as p\n"
									. "    ON ( (a.namespace = '__PageGroup' AND a.page_id = CAST(p.pg_id AS CHAR)) OR (p.pg_id IS NULL) )\n"
									. "  WHERE ( a.target_type = " . ACL_TYPE_USER . " OR a.target_type = " . ACL_TYPE_GROUP . " )\n"
									. "  GROUP BY a.rule_id, $acl_columns, $users_columns, $groups_columns, $pg_columns\n"
									. "  ORDER BY a.target_type ASC, a.rule_id ASC;"
								);
					
					if ( !$q )
						$db->_die();
					
					while ( $row = $db->fetchrow($q) )
					{
						if ( $row['target_type'] == ACL_TYPE_USER && empty($row['username']) )
						{
							// This is only done if we have an ACL affecting a user that doesn't exist.
							// Nice little bit of maintenance to have.
							if ( !$db->sql_query("DELETE FROM " . table_prefix . "acl WHERE rule_id = {$row['rule_id']};") )
								$db->_die();
							continue;
						}
						$score = get_acl_rule_score($row['rules']);
						$deep_limit = ACL_SCALE_MINIMAL_SHADE;
						// Determine background color of cell by score
						if ( $score > 5 )
						{
							// high score, show in green
							$color = 2.5 * $score;
							if ( $color > 255 )
								$color = 255;
							$color = round($color);
							// blend with the colordepth limit
							$color = $deep_limit + ( ( 0xFF - $deep_limit ) - ( ( $color / 0xFF ) * ( 0xFF - $deep_limit ) ) );
							$color = dechex($color);
							$color = "{$color}ff{$color}";
						}
						else if ( $score < -5 )
						{
							// low score, show in red
							$color = 0 - $score;
							$color = 2.5 * $color;
							if ( $color > 255 )
								$color = 255;
							$color = round($color);
							// blend with the colordepth limit
							$color = $deep_limit + ( ( 0xFF - $deep_limit ) - ( ( $color / 0xFF ) * ( 0xFF - $deep_limit ) ) );
							$color = dechex($color);
							$color = "ff{$color}{$color}";
						}
						else
						{
							$color = 'efefef';
						}
						
						// Rate rule textually based on its score
						if ( $score >= 70 )
							$desc = $lang->get('acl_msg_scale_allow');
						else if ( $score >= 50 )
							$desc = $lang->get('acl_msg_scale_mostly_allow');
						else if ( $score >= 25 )
							$desc = $lang->get('acl_msg_scale_some_allow');
						else if ( $score >= -25 )
							$desc = $lang->get('acl_msg_scale_mixed');
						else if ( $score <= -70 )
							$desc = $lang->get('acl_msg_scale_deny');
						else if ( $score <= -50 )
							$desc = $lang->get('acl_msg_scale_mostly_deny');
						else if ( $score <= -25 )
							$desc = $lang->get('acl_msg_scale_some_deny');
						
						// group and user target info
						$info = '';
						if ( $row['target_type'] == ACL_TYPE_USER )
							$info = $lang->get('acl_msg_list_user', array( 'username' => $row['username'] )); // "(User: {$row['username']})";
						else if ( $row['target_type'] == ACL_TYPE_GROUP )
							$info = $lang->get('acl_msg_list_group', array( 'group' => $row['group_name'] ));
						
						// affected pages info
						if ( $row['page_id'] && $row['namespace'] && $row['namespace'] != '__PageGroup' )
							$info .= $lang->get('acl_msg_list_on_page', array( 'page_name' => "{$row['namespace']}:{$row['page_id']}" ));
						else if ( $row['page_id'] && $row['namespace'] && $row['namespace'] == '__PageGroup' )
							$info .= $lang->get('acl_msg_list_on_page_group', array( 'page_group' => $row['pg_name'] ));
						else
							$info .= $lang->get('acl_msg_list_entire_site');
							
						$score_string = $lang->get('acl_msg_list_score', array
							(
								'score' => $score,
								'desc'  => $desc,
								'info'  => $info
								));
						$return['rules'][] = array(
							'score_string' => $score_string,
							'rule_id'      => $row['rule_id'],
							'color'        => $color
							);
					}
					
					break;
				case 'list_presets':
					$presets = array();
					$q = $db->sql_query('SELECT page_id AS preset_name, rule_id, rules FROM ' . table_prefix . "acl WHERE target_type = " . ACL_TYPE_PRESET . ";");
					if ( !$q )
						$db->die_json();
					
					while ( $row = $db->fetchrow() )
					{
						$row['rules'] = $session->string_to_perm($row['rules']);
						$presets[] = $row;
					}
					
					return array(
						'mode' => 'list_existing',
						'presets' => $presets
					);
					break;
				case 'save_preset':
					if ( empty($parms['preset_name']) )
					{
						return array(
							'mode' => 'error',
							'error' => $lang->get('acl_err_preset_name_empty')
						);
					}
					$preset_name = $db->escape($parms['preset_name']);
					$q = $db->sql_query('DELETE FROM ' . table_prefix . "acl WHERE target_type = " . ACL_TYPE_PRESET . " AND page_id = '$preset_name';");
					if ( !$q )
						$db->die_json();
					
					$perms = $session->perm_to_string($parms['perms']);
					if ( !$perms )
					{
						return array(
							'mode' => 'error',
							'error' => $lang->get('acl_err_preset_is_blank')
						);
					}
					
					$perms = $db->escape($perms);
					$q = $db->sql_query('INSERT INTO ' . table_prefix . "acl(page_id, target_type, rules) VALUES\n"
														. "  ( '$preset_name', " . ACL_TYPE_PRESET . ", '$perms' );");
					if ( !$q )
						$db->die_json();
					
					return array(
							'mode' => 'success'
						);
					break;
				case 'trace':
					list($targetpid, $targetns) = RenderMan::strToPageID($parms['page']);
					try
					{
						$perms = $session->fetch_page_acl_user($parms['user'], $targetpid, $targetns);
						$perm_table = array(
								AUTH_ALLOW => 'acl_lbl_field_allow',
								AUTH_WIKIMODE => 'acl_lbl_field_wikimode',
								AUTH_DISALLOW => 'acl_lbl_field_disallow',
								AUTH_DENY => 'acl_lbl_field_deny'
							);
						
						$return = array(
							'mode' => 'trace',
							'perms' => array()
						);
						
						foreach ( $perms->perm_resolve_table as $perm_type => $lookup_data )
						{
							if ( !$session->check_acl_scope($perm_type, $targetns) )
								continue;
							
							$src_l10n = $lang->get($session->acl_inherit_lang_table[$lookup_data['src']], $lookup_data);
							$divclass = preg_replace('/^acl_inherit_/', '', $session->acl_inherit_lang_table[$lookup_data['src']]);
							$perm_string = $lang->get($perm_table[$perms->perms[$perm_type]]);
							$perm_name = $lang->get($session->acl_descs[$perm_type]);
							
							$return['perms'][$perm_type] = array(
									'divclass' => "acl_inherit acl_$divclass",
									'perm_type' => $perm_type,
									'perm_name' => $perm_name,
									'perm_value' => $perm_string,
									'perm_src' => $src_l10n,
									'rule_id' => intval($lookup_data['rule_id']),
									'bad_deps' => $perms->acl_check_deps($perm_type, true)
								);
						}
						
						// group rules if possible
						$return['groups'] = array();
						foreach ( $return['perms'] as $rule )
						{
							if ( !isset($return['groups'][$rule['rule_id']]) )
							{
								$return['groups'][$rule['rule_id']] = array();
							}
							$return['groups'][$rule['rule_id']][] = $rule['perm_type'];
						}
					}
					catch ( Exception $e )
					{
						$return = array(
								'mode' => 'error',
								'error' => $e->getMessage()
							);
					}
					
					break;
				default:
					return Array('mode'=>'error','error'=>'Hacking attempt');
					break;
			}
		}
		return $return;
	}
	
	/**
 	* Same as PageUtils::acl_editor(), but the parms are a JSON string instead of an array. This also returns a JSON string.
 	* @param string $parms Same as PageUtils::acl_editor/$parms, but should be a valid JSON string.
 	* @return string
 	*/
 	
	public static function acl_json($parms = '{ }')
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		try
		{
			$parms = enano_json_decode($parms);
		}
		catch ( Zend_Json_Exception $e )
		{
			$parms = array();
		}
		$ret = PageUtils::acl_editor($parms);
		$ret = enano_json_encode($ret);
		return $ret;
	}
	
	/**
 	* A non-Javascript frontend for the ACL API.
 	* @param array The request data, if any, this should be in the format required by PageUtils::acl_editor()
 	*/
 	
	public static function aclmanager($parms)
	{
		global $db, $session, $paths, $template, $plugins; // Common objects
		global $lang;
		ob_start();
		// Convenience
		$formstart = '<form 
										action="' . makeUrl($paths->page, 'do=aclmanager', true) . '"
										method="post" enctype="multipart/form-data"
										onsubmit="if(!submitAuthorized) return false;"
										>';
		$formend   = '</form>';
		$parms    = PageUtils::acl_preprocess($parms);
		$response = PageUtils::acl_editor($parms);
		$response = PageUtils::acl_postprocess($response);
		
		//die('<pre>' . htmlspecialchars(print_r($response, true)) . '</pre>');
		
		switch($response['mode'])
		{
			case 'debug':
				echo '<pre>' . htmlspecialchars($response['text']) . '</pre>';
				break;
			case 'stage1':
				echo '<h3>' . $lang->get('acl_lbl_welcome_title') . '</h3>
							<p>' . $lang->get('acl_lbl_welcome_body') . '</p>';
				echo $formstart;
				echo '<p><label><input type="radio" name="data[target_type]" value="' . ACL_TYPE_GROUP . '" checked="checked" /> ' . $lang->get('acl_radio_usergroup') . '</label></p>
							<p><select name="data[target_id_grp]">';
				foreach ( $response['groups'] as $group )
				{
					echo '<option value="' . $group['id'] . '">' . $group['name'] . '</option>';
				}
				
				// page group selector
				$groupsel = '';
				if ( count($response['page_groups']) > 0 )
				{
					$groupsel = '<p><label><input type="radio" name="data[scope]" value="page_group" /> ' . $lang->get('acl_radio_scope_pagegroup') . '</label></p>
 											<p><select name="data[pg_id]">';
					foreach ( $response['page_groups'] as $grp )
					{
						$groupsel .= '<option value="' . $grp['id'] . '">' . htmlspecialchars($grp['name']) . '</option>';
					}
					$groupsel .= '</select></p>';
				}
				
				echo '</select></p>
							<p><label><input type="radio" name="data[target_type]" value="' . ACL_TYPE_USER . '" /> ' . $lang->get('acl_radio_user') . '</label></p>
							<p>' . $template->username_field('data[target_id_user]') . '</p>
							<p>' . $lang->get('acl_lbl_scope') . '</p>
							<p><label><input name="data[scope]" value="only_this" type="radio" checked="checked" /> ' . $lang->get('acl_radio_scope_thispage') . '</p>
							' . $groupsel . '
							<p><label><input name="data[scope]" value="entire_site" type="radio" /> ' . $lang->get('acl_radio_scope_wholesite') . '</p>
							<div style="margin: 0 auto 0 0; text-align: right;">
								<input name="data[mode]" value="seltarget" type="hidden" />
								<input type="hidden" name="data[page_id]" value="' . $paths->page_id . '" />
								<input type="hidden" name="data[namespace]" value="' . $paths->namespace . '" />
								<input type="submit" value="' . htmlspecialchars($lang->get('etc_wizard_next')) . '" />
							</div>';
				echo $formend;
				break;
			case 'success':
				echo '<div class="info-box">
								<b>' . $lang->get('acl_lbl_save_success_title') . '</b><br />
								' . $lang->get('acl_lbl_save_success_body', array( 'target_name' => $response['target_name'] )) . '<br />
								' . $formstart . '
								<input type="hidden" name="data[mode]" value="seltarget" />
								<input type="hidden" name="data[target_type]" value="' . $response['target_type'] . '" />
								<input type="hidden" name="data[target_id_user]" value="' . ( ( intval($response['target_type']) == ACL_TYPE_USER ) ? $response['target_name'] : $response['target_id'] ) .'" />
								<input type="hidden" name="data[target_id_grp]"  value="' . ( ( intval($response['target_type']) == ACL_TYPE_USER ) ? $response['target_name'] : $response['target_id'] ) .'" />
								<input type="hidden" name="data[scope]" value="' . ( ( $response['page_id'] ) ? 'only_this' : 'entire_site' ) . '" />
								<input type="hidden" name="data[page_id]" value="' . ( ( $response['page_id'] ) ? $response['page_id'] : 'false' ) . '" />
								<input type="hidden" name="data[namespace]" value="' . ( ( $response['namespace'] ) ? $response['namespace'] : 'false' ) . '" />
								<input type="submit" value="' . $lang->get('acl_btn_returnto_editor') . '" /> <input type="submit" name="data[act_go_stage1]" value="' . $lang->get('acl_btn_returnto_userscope') . '" />
								' . $formend . '
							</div>';
				break;
			case 'delete':
				echo '<div class="info-box">
								<b>' . $lang->get('acl_lbl_delete_success_title') . '</b><br />
								' . $lang->get('acl_lbl_delete_success_body', array('target_name' => $response['target_name'])) . '<br />
								' . $formstart . '
								<input type="hidden" name="data[mode]" value="seltarget" />
								<input type="hidden" name="data[target_type]" value="' . $response['target_type'] . '" />
								<input type="hidden" name="data[target_id_user]" value="' . ( ( intval($response['target_type']) == ACL_TYPE_USER ) ? $response['target_name'] : $response['target_id'] ) .'" />
								<input type="hidden" name="data[target_id_grp]"  value="' . ( ( intval($response['target_type']) == ACL_TYPE_USER ) ? $response['target_name'] : $response['target_id'] ) .'" />
								<input type="hidden" name="data[scope]" value="' . ( ( $response['page_id'] ) ? 'only_this' : 'entire_site' ) . '" />
								<input type="hidden" name="data[page_id]" value="' . ( ( $response['page_id'] ) ? $response['page_id'] : 'false' ) . '" />
								<input type="hidden" name="data[namespace]" value="' . ( ( $response['namespace'] ) ? $response['namespace'] : 'false' ) . '" />
								<input type="submit" value="' . $lang->get('acl_btn_returnto_editor') . '" /> <input type="submit" name="data[act_go_stage1]" value="' . $lang->get('acl_btn_returnto_userscope') . '" />
								' . $formend . '
							</div>';
				break;
			case 'seltarget':
				if ( $response['type'] == 'edit' )
				{
					echo '<h3>' . $lang->get('acl_lbl_editwin_title_edit') . '</h3>';
				}
				else
				{
					echo '<h3>' . $lang->get('acl_lbl_editwin_title_create') . '</h3>';
				}
				$type  = ( $response['target_type'] == ACL_TYPE_GROUP ) ? $lang->get('acl_target_type_group') : $lang->get('acl_target_type_user');
				$scope = ( $response['page_id'] ) ? ( $response['namespace'] == '__PageGroup' ? $lang->get('acl_scope_type_pagegroup') : $lang->get('acl_scope_type_thispage') ) : $lang->get('acl_scope_type_wholesite');
				$subs = array(
						'target_type' => $type,
						'target' => $response['target_name'],
						'scope_type' => $scope
					);
				echo $lang->get('acl_lbl_editwin_body', $subs);
				echo $formstart;
				$parser = $template->makeParserText( $response['template']['acl_field_begin'] );
				echo $parser->run();
				$parser = $template->makeParserText( $response['template']['acl_field_item'] );
				$cls = 'row2';
				foreach ( $response['acl_types'] as $acl_type => $value )
				{
					$vars = Array(
							'FIELD_INHERIT_CHECKED' => '',
							'FIELD_DENY_CHECKED' => '',
							'FIELD_DISALLOW_CHECKED' => '',
							'FIELD_WIKIMODE_CHECKED' => '',
							'FIELD_ALLOW_CHECKED' => '',
						);
					$cls = ( $cls == 'row1' ) ? 'row2' : 'row1';
					$vars['ROW_CLASS'] = $cls;
					
					switch ( $response['current_perms'][$acl_type] )
					{
						case 'i':
						default:
							$vars['FIELD_INHERIT_CHECKED'] = 'checked="checked"';
							break;
						case AUTH_ALLOW:
							$vars['FIELD_ALLOW_CHECKED'] = 'checked="checked"';
							break;
						case AUTH_WIKIMODE:
							$vars['FIELD_WIKIMODE_CHECKED'] = 'checked="checked"';
							break;
						case AUTH_DISALLOW:
							$vars['FIELD_DISALLOW_CHECKED'] = 'checked="checked"';
							break;
 						case AUTH_DENY:
							$vars['FIELD_DENY_CHECKED'] = 'checked="checked"';
							break;
					}
					$vars['FIELD_NAME'] = 'data[perms][' . $acl_type . ']';
					if ( preg_match('/^([a-z0-9_]+)$/', $response['acl_descs'][$acl_type]) )
					{
						$vars['FIELD_DESC'] = $lang->get($response['acl_descs'][$acl_type]);
					}
					else
					{
						$vars['FIELD_DESC'] = $response['acl_descs'][$acl_type];
					}
					$parser->assign_vars($vars);
					echo $parser->run();
				}
				$parser = $template->makeParserText( $response['template']['acl_field_end'] );
				echo $parser->run();
				echo '<div style="margin: 10px auto 0 0; text-align: right;">
								<input name="data[mode]" value="save_' . $response['type'] . '" type="hidden" />
								<input type="hidden" name="data[page_id]" value="'   . (( $response['page_id']   ) ? $response['page_id']   : 'false') . '" />
								<input type="hidden" name="data[namespace]" value="' . (( $response['namespace'] ) ? $response['namespace'] : 'false') . '" />
								<input type="hidden" name="data[target_type]" value="' . $response['target_type'] . '" />
								<input type="hidden" name="data[target_id]" value="' . $response['target_id'] . '" />
								<input type="hidden" name="data[target_name]" value="' . $response['target_name'] . '" />
								' . ( ( $response['type'] == 'edit' ) ? '<input type="submit" value="' . $lang->get('etc_save_changes') . '" />&nbsp;&nbsp;<input type="submit" name="data[act_delete_rule]" value="' . $lang->get('acl_btn_deleterule') . '" style="color: #AA0000;" onclick="return confirm(\'' . addslashes($lang->get('acl_msg_deleterule_confirm')) . '\');" />' : '<input type="submit" value="' . $lang->get('acl_btn_createrule') . '" />' ) . '
							</div>';
				echo $formend;
				break;
			case 'error':
				ob_end_clean();
				die_friendly('Error occurred', '<p>Error returned by permissions API:</p><pre>' . htmlspecialchars($response['error']) . '</pre>');
				break;
		}
		$ret = ob_get_contents();
		ob_end_clean();
		echo
			$template->getHeader() .
			$ret .
			$template->getFooter();
	}
	
	/**
 	* Preprocessor to turn the form-submitted data from the ACL editor into something the backend can handle
 	* @param array The posted data
 	* @return array
 	* @access private
 	*/
 	
	public static function acl_preprocess($parms)
	{
		if ( !isset($parms['mode']) )
			// Nothing to do
			return $parms;
		switch ( $parms['mode'] )
		{
			case 'seltarget':
				
				// Who's affected?
				$parms['target_type'] = intval( $parms['target_type'] );
				$parms['target_id'] = ( $parms['target_type'] == ACL_TYPE_GROUP ) ? $parms['target_id_grp'] : $parms['target_id_user'];
				
			case 'save_edit':
			case 'save_new':
				if ( isset($parms['act_delete_rule']) )
				{
					$parms['mode'] = 'delete';
				}
				
				// Scope (just this page or entire site?)
				if ( $parms['scope'] == 'entire_site' || ( $parms['page_id'] == 'false' && $parms['namespace'] == 'false' ) )
				{
					$parms['page_id']   = false;
					$parms['namespace'] = false;
				}
				else if ( $parms['scope'] == 'page_group' )
				{
					$parms['page_id'] = $parms['pg_id'];
					$parms['namespace'] = '__PageGroup';
				}
				
				break;
		}
		
		if ( isset($parms['act_go_stage1']) )
		{
			$parms = array(
					'mode' => 'listgroups'
				);
		}
		
		return $parms;
	}
	
	public static function acl_postprocess($response)
	{
		if(!isset($response['mode']))
		{
			if ( isset($response['groups']) )
				$response['mode'] = 'stage1';
			else
				$response = Array(
						'mode' => 'error',
						'error' => 'Invalid action passed by API backend.',
					);
		}
		return $response;
	}
 	
}

/**
 * Generates a graphical key showing how the ACL rule list works.
 * @return string
 */

function acl_list_draw_key()
{
	$out  = '<div style="width: 460px; margin: 0 auto; text-align: center; margin-bottom: 10px;">';
	$out .= '<div style="float: left;">&larr; Deny</div>';
	$out .= '<div style="float: right;">Allow &rarr;</div>';
	$out .= 'Neutral';
	$out .= '<div style="clear: both;"></div>';
	// 11 boxes on each side of the center
	$inc = ceil ( ( 0xFF - ACL_SCALE_MINIMAL_SHADE ) / 11 );
	for ( $i = ACL_SCALE_MINIMAL_SHADE; $i <= 0xFF; $i+= $inc )
	{
		$octet = dechex($i);
		$color = "ff$octet$octet";
		$out .= '<div style="background-color: #' . $color . '; float: left; width: 20px;">&nbsp;</div>';
	}
	$out .= '<div style="background-color: #efefef; float: left; width: 20px;">&nbsp;</div>';
	for ( $i = 0xFF; $i >= ACL_SCALE_MINIMAL_SHADE; $i-= $inc )
	{
		$octet = dechex($i);
		$color = "{$octet}ff{$octet}";
		$out .= '<div style="background-color: #' . $color . '; float: left; width: 20px;">&nbsp;</div>';
	}
	$out .= '<div style="clear: both;"></div>';
	$out .= '<div style="float: left;">-100</div>';
	$out .= '<div style="float: right;">+100</div>';
	$out .= '0';
	$out .= '</div>';
	return $out;
}

/**
 * Gets the numerical score for the serialized form of an ACL rule
 */

function get_acl_rule_score($perms)
{
	global $db, $session, $paths, $template, $plugins; // Common objects
	if ( is_string($perms) )
		$perms = $session->string_to_perm($perms);
	else if ( !is_array($perms) )
		return false;
	$score = 0;
	foreach ( $perms as $item )
	{
		switch ( $item )
		{
			case AUTH_ALLOW :
				$inc = 2;
				break;
			case AUTH_WIKIMODE:
				$inc = 1;
				break;
			case AUTH_DISALLOW:
				$inc = -1;
				break;
			case AUTH_DENY:
				$inc = -2;
				break;
			default:
				$inc = 0;
				break;
		}
		$score += $inc;
	}
	// this is different from the beta; calculate highest score and
	// get percentage to be fairer to smaller/less broad rules
	$divisor = count($perms) * 2;
	if ( $divisor == 0 )
	{
		return 0;
	}
	$score = 100 * ( $score / $divisor );
	return round($score);
}

?>