<?php
/*
Plugin Name: WordPress XHTML validator
Plugin URI: http://rudd-o.com/projects/wp-validator/
Description: Validates posts, pages and comments against XHTML 1.0 — <a href="http://rudd-o.com/projects/wp-validator/wp-validator-usage/">Usage instructions</a> — Don't forget to <a href="http://rudd-o.com/projects/wp-validator/campaign/">join the Truly Valid campaign</a> today!
Version: 0.2.0
Author: Manuel Amador (Rudd-O)
Author URI: http://rudd-o.com/
*/

/*  Copyright 2005  Manuel Amador

Derivative work from bsuite, written by Casey Bisson

    This program is free software; you can redistribute it 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 more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

/* TODO

enhance wp validator so it can show status of validation on comment editor pages, and it should also validate textarea contents on command with a nice small button

*/

function validator_campaign_short() {

	print '<a title="This WordPress blog truly validates as XHTML 1.0 thanks to the WordPress XHTML validator" href="http://rudd-o.com/projects/wp-validator/campaign/"><em>Truly Valid</em></a> <a href="http://validator.w3.org/check/referer" title="Validate this page now"><abbr title="eXtensible HyperText Markup Language">XHTML</abbr></a>';

}

function validator_campaign_long() {

	print 'This page truly <a href="http://validator.w3.org/check/referer" title="Validate this page now">validates</a> as <abbr title="eXtensible HyperText Markup Language">XHTML</abbr> 1.0 thanks to the <a href="http://rudd-o.com/projects/wp-validator/" title="The WordPress XHTML validator home page">WordPress XHTML validator</a>.  Join the <a href="http://rudd-o.com/projects/wp-validator/campaign/"><em>Truly Valid</em></a> campaign today!';

}

/* UTILITY FUNCTIONS */

$validator_entity_manifest = file_get_contents(dirname(__FILE__)."/entity-manifest.txt");
$validator_entity_manifest = preg_replace("/<!--.+?-->/s","",$validator_entity_manifest);
$validator_entity_manifest = preg_replace("/ --.+?--/s","",$validator_entity_manifest);
$validator_entity_manifest = '[' . $validator_entity_manifest . ']';


$validate_encoding = get_settings("blog_charset");
$validate_header = '<?xml version="1.0" encoding="'.$validate_encoding.'" ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" '.$validator_entity_manifest.' >
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset='.$validate_encoding.'"/>
<title>Dummy title</title>
</head>
<body><div>';
$validate_footer = '</div></body>
</html>';

function hc($output) {
	global $validate_encoding;
	return htmlspecialchars($output,ENT_QUOTES,$validate_encoding);
}

function originalcontent_output($o) {
	if (trim($o))
	return "<p>This is the original markup:</p><pre>".hc($o)."</pre>";
	return "";
}

function renderedcontent_output($o) {
	if (trim($o))
	return "<p>This is the markup rendered by your plugins:</p><pre>".hc($o)."</pre>";
	return "";
}

function xmllint_output($err) {
	if (trim($err))
	return "<p><code>xmllint</code> found the following errors:</p><pre>".hc($err)."</pre>";
	return "";
}

function tidy_output($err) {
	if (trim($err))
	return "<p><code>tidy</code> found the following errors:</p><pre>".hc($err)."</pre>";
	return "";
}

function xmllint_suggestion($suggestion) {
	if (trim($suggestion))
	return "<p><code>xmllint</code> has the following suggestion:</p><pre>".hc($suggestion)."</pre>";
	return "";
}

function tidy_suggestion($suggestion) {
	if (trim($suggestion))
	return "<p><code>tidy</code> has the following suggestion:</p><pre>".hc($suggestion)."</pre>";
	return "";
}

function replacecrlf($content) {
	return str_replace(chr(13) . chr(10),chr(10),$content); // replace crlf for lf only, otherwise xmllint gets anxious
}

function validator_wrap_content($content) {
	global $validate_header,$validate_footer;
	return $validate_header . $content . $validate_footer;
}

function validator_filter_content($content,$filtername) {
	return apply_filters($filtername, str_replace(']]>', ']]&gt;', $content));
}

function test_for_xmllint() {
	global $xmllint_available;
	if (!isset($xmllint_available)) {
		$retval = 0;
		$output = array();
		$rettext = exec('xmllint',$output,$retval);
		if ($retval == 127) $xmllint_available = false;
		else $xmllint_available = true;
	}
	return $xmllint_available;
}

function test_for_tidy() {
	global $tidy_available;
	if (!isset($tidy_available)) {
		$retval = 0;
		$output = array();
 		$rettext = exec('tidy -?',$output,$retval);
		if ($retval == 127) $tidy_available = false;
		else $tidy_available = true;
	}
	return $tidy_available;
}

/* start validation core */

function run_content_through_xmllint($content) {
	if (trim($content) == "") return array("",""); //bypass if empty

	global $validate_encoding;

	$cmd = "nice xmllint --nonet --xmlout --encode $validate_encoding -";
	$process = proc_open($cmd, array(0 => array("pipe", "r"),1 => array("pipe", "w"),2 => array("pipe", "w")), $pipes);

	$stdout = ""; $stderr = "";
	if (!is_resource($process)) die("Failed to run xmllint");

	// wrap the content to get consistent results
	$content = validator_wrap_content($content);

	fwrite($pipes[0], $content);
	fclose($pipes[0]);
	while(!feof($pipes[1]) || !feof($pipes[2])) {
		if (!feof($pipes[1])) $stdout .= fgets($pipes[1], 1024);
		if (!feof($pipes[2])) $stderr .= fgets($pipes[2], 1024);
	}
	// munge the suggestion
	if (preg_match("|<body><div>(.*?)</div></body>|s",$stdout,$matches)) {
		$stdout = $matches[1];
	}
	$stdout = str_replace("<!--","\n\n<!--",$stdout);
	$stdout = str_replace("-->","-->\n\n",$stdout);

	return array(0=>$stderr,1=>$stdout);
}

function run_content_through_tidy($content) {
	if (trim($content) == "") return array("",""); //bypass if empty

	global $validate_encoding;
	$encodings = array("utf-8"=>"-utf8","latin1"=>"-latin1","latin-1"=>"-latin1","iso-8859-1"=>"-latin1"); // FIXME add more encodings
	if (array_key_exists(strtolower($validate_encoding),$encodings))
		$encodingarg = $encodings[strtolower($validate_encoding)];
	else $encodingarg = "";

	$cmd = "nice tidy -q $encodingarg -";
	$process = proc_open($cmd, array(0 => array("pipe", "r"),1 => array("pipe", "w"),2 => array("pipe", "w")), $pipes);

	$stdout = ""; $stderr = "";
	if (!is_resource($process)) die("Failed to run tidy");

	// wrap the content to get consistent results
	$content = validator_wrap_content($content);

	fwrite($pipes[0], $content);
	fclose($pipes[0]);
	while(!feof($pipes[1]) || !feof($pipes[2])) {
		if (!feof($pipes[1])) $stdout .= fgets($pipes[1], 1024);
		if (!feof($pipes[2])) $stderr .= fgets($pipes[2], 1024);
	}
	// munge the suggestion
	if (preg_match("|<body>
<div>(.*?)</div>
</body>|s",$stdout,$matches)) {
		$stdout = $matches[1];
	}
	$stdout = str_replace("<!--","\n\n<!--",$stdout);
	$stdout = str_replace("-->","-->\n\n",$stdout);

	return array(0=>$stderr,1=>$stdout);
}

function run_content_through_tidy_if_available($content){
	if (test_for_tidy()) return run_content_through_tidy($content);
	return array("","");
}

function run_content_through_xmllint_if_available($content){
	if (test_for_xmllint()) return run_content_through_xmllint($content);
	return array("","");
}

/* end validation core */

/* the code for displaying warnings to the user in the post editor */
/* this code is shared with autolink but namespaced to prevent clashes */

$validator_warnings = "";
$validator_warnings_displayed = false;

function validator_warning_footer() { global $validator_warnings; echo $validator_warnings; }
/* position_id is the HTML id of the element after which we want to place this warning */
function validator_display_warning($warning_id,$position_id,$title,$contents) {
	global $validator_warnings;
	$validator_warnings .= "
	<div id='$warning_id' class='updated fade-ff0000'><p><strong>".$title.".</strong> ".$contents."</p></div>
	";
	$validator_warnings .= '<script language="javascript" type="text/javascript">';
	$validator_warnings .= "var thewarning = document.getElementById('$warning_id');";
	$validator_warnings .= "var theelement = document.getElementById('$position_id');";
	$validator_warnings .= "if (theelement) { if (theelement.nextSibling) { theelement.parentNode.insertBefore(thewarning,theelement.nextSibling); } else { theelement.parentNode.appendChild(thewarning); } }";
	$validator_warnings .= '</script>';

	global $validator_warnings_displayed;
	if (!$validator_warnings_displayed) {
		add_action('admin_footer', 'validator_warning_footer');
		$validator_warnings_displayed = true;
	}
}

/* end admin communication code */

/* the code for validating the post while it is being edited */

function validate_single_post($id) { /* returns zero if okay, 1 if invalid, 2 if filter fuckup */
	global $wpdb;
	$content = $wpdb->get_var("SELECT post_content from $wpdb->posts where ID = $id limit 1");
	list($xmllint_output,$xmllint_suggestion) = run_content_through_xmllint_if_available($content);
	list($tidy_output,$tidy_suggestion) = run_content_through_tidy_if_available($content);

	if (!$xmllint_output && !$tidy_output) {
		$content = validator_filter_content($content,'the_content');
		list($xmllint_output,$xmllint_suggestion) = run_content_through_xmllint_if_available($content);
		list($tidy_output,$tidy_suggestion) = run_content_through_tidy_if_available($content);
		
		if (!$xmllint_output && !$tidy_output) return 0;
		else return 2;
	} else return 1;
}

function validate_single_post_title($id) { /* returns zero if okay, 1 if invalid, 2 if filter fuckup */
	global $wpdb;
	$content = $wpdb->get_var("SELECT post_title from $wpdb->posts where ID = $id limit 1");
	list($xmllint_output,$xmllint_suggestion) = run_content_through_xmllint_if_available($content);
	list($tidy_output,$tidy_suggestion) = run_content_through_tidy_if_available($content);

	if (!$xmllint_output && !$tidy_output) {
		$content = validator_filter_content(validator_filter_content($content,'get_the_title'),'the_title');
		list($xmllint_output,$xmllint_suggestion) = run_content_through_xmllint_if_available($content);
		list($tidy_output,$tidy_suggestion) = run_content_through_tidy_if_available($content);
		
		if (!$xmllint_output && !$tidy_output) return 0;
		else return 2;
	} else return 1;
}

function validation_on_edit_post() {

	$validationresult = validate_single_post(intval($_GET["post"]));
	if ($validationresult == 1) {
		$reason = "The contents of this entry contain invalid XHTML.  Please fix the entry, and then hit ".__("Save and Continue Editing");
	} elseif ($validationresult == 2) {
		$reason = "The contents of this entry are being rendered invalid by an active filter plugin &mdash; when browsed by readers, the blog page corresponding to this entry displays invalid XHTML.  Try deactivating your filter plugins one by one to find the source of the problem.";
	} else { $reason = ""; /* everything is okay */
	}
	if ($reason) validator_display_warning("validator-warning-invalidtitle","write-post","Invalid XHTML in entry",$reason);

	$validationresult = validate_single_post_title(intval($_GET["post"]));
	if ($validationresult == 1) {
		$reason = "The title of this entry contains invalid XHTML.  Please fix the title, and then hit ".__("Save and Continue Editing");
	} elseif ($validationresult == 2) {
		$reason = "The title of this entry is being rendered invalid by an active filter plugin &mdash; when browsed by readers, the blog page corresponding to this entry displays invalid XHTML.  Try deactivating your filter plugins one by one to find the source of the problem.";
	} else { $reason = ""; /* everything is okay */
	}
	if ($reason) validator_display_warning("validator-warning-invalidtitle","write-post","Invalid XHTML in entry",$reason);

}

function validation_validate_post_if_appropriate() {
	if(strpos($_SERVER['REQUEST_URI'], 'post.php'))
	{
		if ($_GET["action"] == "edit" && intval($_GET["post"])) {
			validation_on_edit_post();
		}
	}
}

/* end code for validation on the editor */

/* manage screen: progress bar functions */

$al_progress_counter = 0;
$al_total_count = 0;
$al_report_interval = 0;
$al_progress_style = "<style type='text/css'>#progress-bar-container {border: 1px solid #A3A3A3;background:#F2F2F2;}
#progress-bar {border: 1px solid #A3A3A3;background: #A3A3A3;height: 1em;}</style>";


function validator_report_make_progress_bar() {
global $al_progress_style; print $al_progress_style;?>
<h4 style="text-align:center; margin-bottom: 0.1em; padding-bottom: 0em;"><span id="progress-status">Process initiating...</span></h4><div id="progress-bar-container"><div id="progress-bar"></div></div>
<script language="JavaScript" type="text/javascript">
function updateStatusBar(newStatusText) {
	var vsnode = document.getElementById("progress-bar");
	var style = "width: " + newStatusText + "%;";
	vsnode.setAttribute("style",style);
}
function us(progress,total,intext) {
	var vsnode = document.getElementById("progress-status");
	while (vsnode.childNodes.length > 0) vsnode.removeChild(vsnode.childNodes[0]);
	vsnode.appendChild(document.createTextNode("Processing entry " + progress + " out of " + total + " entries..."));
	updateStatusBar(intext);
}
function deleteStatus() {
	vsnode = document.getElementById("progress-bar-container");
	vsnode.parentNode.removeChild(vsnode);
	var vsnode = document.getElementById("progress-status");
	vsnode.parentNode.parentNode.removeChild(vsnode.parentNode);
}
</script>
<?
}

function validator_report_start($total_count) {
	global $al_total_count;
	global $al_report_interval;
	global $al_progress_counter;
	$al_total_count = $total_count;
	if ($al_total_count == 0) $al_total_count = 1; // prevent division by zero
	$al_report_interval = intval($total_count/100);
	if ($al_report_interval == 0) $al_report_interval = 1; // prevent division by zero
	$al_progress_counter = 0;
	print "\n";
	validator_report_make_progress_bar(); // to bootstrap the required code)
	print "\n";
	print '<script>';
	print "us($al_progress_counter,$al_total_count,'0');";
	print '</script>';
	ob_flush();
	flush();
}

function validator_report_finish() {
	print "\n";
	print '<script>';
	print 'deleteStatus();';
	print '</script>';
	ob_flush();
	flush();
}

function validator_report_progress($id) {
	global $al_progress_counter;
	global $al_total_count;
	global $al_report_interval;
	$al_progress_counter = $al_progress_counter + 1;
	if (($al_progress_counter % $al_report_interval == 0)) {
		print "\n";
		print '<script>';
		$intext = intval(intval($al_progress_counter)/intval($al_total_count)*100);
		print "us($al_progress_counter,$al_total_count,'$intext');";
		print '</script>';
		ob_flush();
		flush();
	}
}

/* manage screen: end progress bar functions */



//
// the management menu 
//

// FIXME make a check so the join the campaign today link does not appear if the user already has a link in his homepage to the truly valid campaign

function validator_user_requested_validation() {
	if (array_key_exists("xhtmlvalidate",$_POST)) return true;
	return false;
}

function validator_user_requested_mass_fix() {
	if (array_key_exists("xhtmlvalidatefix",$_POST)) return true;
	return false;
}

function validator_the_edit_post_link($id) {
	global $user_ID; get_currentuserinfo();
	if ( !user_can_edit_post($user_ID, $id) ) {
		return;
	}
	else {
		$link = "Edit";
		$location = get_settings('siteurl') . "/wp-admin/post.php?action=edit&amp;post=$id";
		return "<a href=\"$location\" class='edit'>$link</a>";
	}
}

function validator_get_post_id_from_comment_id($comment_id) {
        global $wpdb;
        return $wpdb->get_var("select comment_post_ID from $wpdb->comments where comment_ID = $comment_id limit 1;");
}

function validator_the_edit_comment_link($comment_id) {
	global $user_ID; get_currentuserinfo();
	$id = validator_get_post_id_from_comment_id($comment_id);
	if ( !user_can_edit_post_comments($user_ID, $id) ) {
		return;
	}
	else {
		$link = "Edit";
		$location = get_settings('siteurl') . "/wp-admin/post.php?action=editcomment&amp;comment=$comment_id";
		return "<a href=\"$location\" class='edit'>$link</a>";
	}
}

function validator_the_view_post_link($id) {
	return '<a href="' . get_permalink($id) . '" class="edit">' . "View" . "</a>";
}

function validator_the_view_comment_link($comment_id) {
	$id = validator_get_post_id_from_comment_id($comment_id);
	return '<a href="' . get_permalink($id) . '#comment-'.$comment_id.'" class="edit">' . "View" . "</a>";
}

function validator_format_mass_report($report) {
	$togglejs = "<script type='text/javascript' language='javascript'>
	function togglemessages(posttype,postid) {
		var element = document.getElementById('messages-' + posttype + '-' + postid.toString());
		if (element.getAttribute('style') == 'display: none;') {
			element.setAttribute('style','position: fixed; color: black; background: white; width: 80%; height: 80%; overflow: scroll; padding-left: 1em; display: block; top: 10%; left: 10%; border: 2px outset #666666;');
			if (!element.getAttribute('closediv')) {
				var closediv = document.createElement('input');
				closediv.setAttribute('type','button');
				closediv.setAttribute('value','Close');
				closediv.setAttribute('style','float:right; margin: 0;');
				//closediv.appendChild(document.createTextNode('Click here to close this window'));
				closediv.setAttribute('onclick','togglemessages(\"'+posttype+'\",'+postid.toString()+');');
				element.setAttribute('closediv',closediv);
				element.insertBefore(closediv,element.firstChild);
			}
			element.firstChild.focus();
		}
		else {
			element.setAttribute('style','display: none;');
			element.previousSibling.focus();
		}
	}
	</script>";

	$string = "";
	if ($report) {
	    $string = $togglejs;
	    $string .= '<form action="" method="POST"><p class="submit">';
	    $string .= '<table width="100%" style="margin-left: auto; margin-right: auto;" cellpadding="3" cellspacing="3">';
	    $string .= "<th>ID</th><th>Type</th><th>Action</th><th></th><th>Quick fix</th><th></th><th></th>";
	    foreach($report as $r) {
		$eid = $r[0];
		$type = $eid[0];
		$id = $eid[1];
		$report_suggestion = $r[3];
		$alternate = $alternate ? "" : "class='alternate'";
		$string .= "<tr $alternate>";
		$string .=  "<td>{$id}</td>";
		if ($type == "comment") $string .= "<td>Comment</td>";
		else $string .= "<td>Post/page</td>";
		if ($type == "title") {
			if ($r[1] == "invalidxhtml") $msg = "Invalid XHTML found in title";
			elseif ($r[1] == "filterfuckup") $msg = "A plugin is damaging the title";
			elseif ($r[1] == "correct") $msg = "Title is correct";
			else $msg = $r[1];
		} elseif ($type == "post") {
			if ($r[1] == "invalidxhtml") $msg = "Invalid XHTML found in content";
			elseif ($r[1] == "filterfuckup") $msg = "A plugin is damaging the content";
			elseif ($r[1] == "correct") $msg = "Content is correct";
			else $msg = $r[1];
		} elseif ($type == "comment") {
			if ($r[1] == "invalidxhtml") $msg = "Invalid XHTML found in comment";
			elseif ($r[1] == "filterfuckup") $msg = "A plugin is damaging the comment";
			elseif ($r[1] == "correct") $msg = "Comment is correct";
			else $msg = $r[1];
		}
		$string .= "<td nowrap='nowrap'>$msg</td>";
		$messages = $r[2];
		if ($messages) {
			$link = "javascript:togglemessages('{$type}',{$id});";
			$popper = "<a href=\"$link\" class='edit'>Messages</a>";
			$messages = " " . $popper .
				"<div id='messages-{$type}-{$id}' style='display: none;'>$messages</div>";
		}
		$string .= "<td>$messages</td>";
		
		$string .= "<td>";
		$fixups = array();
		if (test_for_tidy() && ($report_suggestion == 1 || $report_suggestion == 3)) $fixups[] = "<label for='fix-$type-$id-tidy'><input type='radio' name='fix-$type-$id' id='fix-$type-$id-tidy' value='tidy'/> Using <code>tidy</code></label>";
		if (test_for_xmllint()  && ($report_suggestion == 2 || $report_suggestion == 3)) $fixups[] = "<label for='fix-$type-$id-xmllint'><input type='radio' name='fix-$type-$id' id='fix-$type-$id-xmllint' value='xmllint'/> Using <code>xmllint</code></label>";
		if (test_for_xmllint() || test_for_tidy()) $fixups[] = "<label for='fix-$type-$id-none'><input type='radio' name='fix-$type-$id' id='fix-$type-$id-none' value='' checked='checked'/> Do not fix</label>";
		$string .= implode("<br/>",$fixups);
		$string .= "</td>";

		if ($type == "post" || $type == "title") $view = validator_the_view_post_link($id);
		else $view = validator_the_view_comment_link($id);
		$string .= "<td>$view</td>";
		if ($type == "post" || $type == "title") $edit = validator_the_edit_post_link($id);
		else $edit = validator_the_edit_comment_link($id);;
		$string .= "<td>$edit</td>";

		$string .= "</tr>";
		print $string; $string = "";
	    }
	    $string .=  "</table>";
	    $string .= "<p class='submit'>" . '<input type="submit" name="xhtmlvalidatefix" value="Fix using selected tool"/>' . "</p></form>";
	    print ($string); $string = "";
	} else {
		print ("<p style='font-weight: bold'>Nothing to report.  No invalid XHTML content found.</p>");
	}
}

function validator_mass_validate($start_callback = null, $finish_callback = null, $progress_callback = null) {

	global $wpdb;
	global $user_ID;
	get_currentuserinfo();

	$report = array();
	$allcontent = array();
	$allcomments = array();

	$sql = "select ID,post_content,post_title from {$wpdb->posts}";
	$wpdb->query($sql);
	$results = $wpdb->get_results();
	foreach ($results as $row) {
		if ($row->ID && user_can_edit_post($user_ID, $row->ID))
			$allcontent[$row->ID] = array(replacecrlf($row->post_title),replacecrlf($row->post_content));
	}

	$sql = "SELECT comment_ID,comment_content from {$wpdb->comments} where comment_approved != 'spam'"; /* fixme add condition to see which ones the user can edit */
	$wpdb->query($sql);
	$results = $wpdb->get_results();
	foreach ($results as $row) {
		if ($row->comment_ID) $allcomments[$row->comment_ID] = replacecrlf($row->comment_content);
	}

	if ($start_callback) $start_callback(sizeof($allcontent) * 2 + sizeof($allcomments));

	/* title validation */
	$filters = array('get_the_title','the_title');
	foreach ($allcontent as $key=>$val) {
		$id = $key;
		$content = $val[0];
		if ($progress_callback) $progress_callback($id);

		$report_entry = array("title",$id);
		$report_action = "none";
		$report_messages = "";
		$report_suggestion = 0; // 1 if tidy, 2 if xmllint, 3 if both

		list ($xmllint_output ,$xmllint_suggestion) = run_content_through_xmllint_if_available($content);
		list ($tidy_output ,$tidy_suggestion) = run_content_through_tidy_if_available($content);

		if (!$tidy_output && !$xmllint_output) { /* if it was correct */

			$filtered_content = $content;
			foreach ($filters as $s) $filtered_content = validator_filter_content($filtered_content,$s);
			list ($xmllint_output ,$xmllint_suggestion) = run_content_through_xmllint_if_available($filtered_content);
			list ($tidy_output ,$tidy_suggestion) = run_content_through_tidy_if_available($filtered_content);

			if (!$tidy_output && !$xmllint_output) { /* then it is truly correct */
				$report_action = "correct";
			} else { /* a filter is fucking up things */
				$report_action = "filterfuckup";
				$report_messages .= xmllint_output($xmllint_output).tidy_output($tidy_output).
					xmllint_suggestion($xmllint_suggestion).tidy_suggestion($tidy_suggestion).
					originalcontent_output($content).renderedcontent_output($filtered_content);
			}

		 } else { /* contents are invalid xhtml */

			$report_action = "invalidxhtml";
			$report_messages .= xmllint_output($xmllint_output).tidy_output($tidy_output).
					xmllint_suggestion($xmllint_suggestion).tidy_suggestion($tidy_suggestion).
					originalcontent_output($content);
			if ($tidy_suggestion) $report_suggestion = 1;
			if ($xmllint_suggestion) $report_suggestion = $report_suggestion + 2;

		}

		if ($report_action != "correct")
			array_push($report,array($report_entry,$report_action,$report_messages,$report_suggestion));
	}
	/* end title validation */

	/* content validation */
	$filters = array('the_content');
	foreach ($allcontent as $key=>$val) {
		$id = $key;
		$content = $val[1];
		if ($progress_callback) $progress_callback($id);

		$report_entry = array("post",$id);
		$report_action = "none";
		$report_messages = "";
		$report_suggestion = 0; // 1 if tidy, 2 if xmllint, 3 if both

		list ($xmllint_output ,$xmllint_suggestion) = run_content_through_xmllint_if_available($content);
		list ($tidy_output ,$tidy_suggestion) = run_content_through_tidy_if_available($content);

		if (!$tidy_output && !$xmllint_output) { /* if it was correct */

			$filtered_content = $content;
			foreach ($filters as $s) $filtered_content = validator_filter_content($filtered_content,$s);
			list ($xmllint_output ,$xmllint_suggestion) = run_content_through_xmllint_if_available($filtered_content);
			list ($tidy_output ,$tidy_suggestion) = run_content_through_tidy_if_available($filtered_content);

			if (!$tidy_output && !$xmllint_output) { /* then it is truly correct */
				$report_action = "correct";
			} else { /* a filter is fucking up things */
				$report_action = "filterfuckup";
				$report_messages .= xmllint_output($xmllint_output).tidy_output($tidy_output).
					xmllint_suggestion($xmllint_suggestion).tidy_suggestion($tidy_suggestion).
					originalcontent_output($content).renderedcontent_output($filtered_content);
			}

		 } else { /* contents are invalid xhtml */

			$report_action = "invalidxhtml";
			$report_messages .= xmllint_output($xmllint_output).tidy_output($tidy_output).
					xmllint_suggestion($xmllint_suggestion).tidy_suggestion($tidy_suggestion).
					originalcontent_output($content);
			if ($tidy_suggestion) $report_suggestion = 1;
			if ($xmllint_suggestion) $report_suggestion = $report_suggestion + 2;

		}

		if ($report_action != "correct")
			array_push($report,array($report_entry,$report_action,$report_messages,$report_suggestion));
	}
	/* end content validation */

	/* comment validation */
	$filters = array('get_comment_text','comment_text');
	foreach ($allcomments as $key=>$val) {
		$id = $key;
		$content = $val;
		if ($progress_callback) $progress_callback($id);

		$report_entry = array("comment",$id);
		$report_action = "none";
		$report_messages = "";
		$report_suggestion = 0; // 1 if tidy, 2 if xmllint, 3 if both

		list ($xmllint_output ,$xmllint_suggestion) = run_content_through_xmllint_if_available($content);
		list ($tidy_output ,$tidy_suggestion) = run_content_through_tidy_if_available($content);

		if (!$tidy_output && !$xmllint_output) { /* if it was correct */

			$filtered_content = $content;
			foreach ($filters as $s) $filtered_content = validator_filter_content($filtered_content,$s);
			list ($xmllint_output ,$xmllint_suggestion) = run_content_through_xmllint_if_available($filtered_content);
			list ($tidy_output ,$tidy_suggestion) = run_content_through_tidy_if_available($filtered_content);

			if (!$tidy_output && !$xmllint_output) { /* then it is truly correct */
				$report_action = "correct";
			} else { /* a filter is fucking up things */
				$report_action = "filterfuckup";
				$report_messages .= xmllint_output($xmllint_output).tidy_output($tidy_output).
					xmllint_suggestion($xmllint_suggestion).tidy_suggestion($tidy_suggestion).
					originalcontent_output($content).renderedcontent_output($filtered_content);
			}

		 } else { /* contents are invalid xhtml */

			$report_action = "invalidxhtml";
			$report_messages .= xmllint_output($xmllint_output).tidy_output($tidy_output).
					xmllint_suggestion($xmllint_suggestion).tidy_suggestion($tidy_suggestion).
					originalcontent_output($content);
			if ($tidy_suggestion) $report_suggestion = 1;
			if ($xmllint_suggestion) $report_suggestion = $report_suggestion + 2;

		}

		if ($report_action != "correct")
			array_push($report,array($report_entry,$report_action,$report_messages,$report_suggestion));
	}
	/* end comment validation */

	if ($finish_callback) $finish_callback();
	return $report;

}

function validator_mass_fix($start_callback = null, $finish_callback = null, $progress_callback = null) {

	global $wpdb;
	global $user_ID;
	get_currentuserinfo();

	$posts = array();
	$titles = array();
	$comments = array();

	foreach ($_POST as $k=>$fixuptype) {
		if (substr($k,0,4) == "fix-") {
			if (substr($k,4,strlen("comment")) == "comment") {
				$type = "comment";
			} elseif (substr($k,4,strlen("post")) == "post") {
				$type = "post";
			} elseif (substr($k,4,strlen("title")) == "title") {
				$type = "title";
			}
			else trigger_error("No sir, we're not taking a post keyed $k",E_USER_ERROR);
			$id = substr($k,4+strlen($type)+1);
			if (!is_numeric($id)) trigger_error("No sir, we're not taking a post keyed $k",E_USER_ERROR);
			$id = intval($id);
			if ($fixuptype != "" && $fixuptype != "xmllint" && $fixuptype != "tidy")
				trigger_error("No sir, we're not taking a post keyed $k with fixup type $fixuptype",E_USER_ERROR);
			if ($type == "post" && $fixuptype != "") $posts[] = array($id,$fixuptype);
			elseif ($type == "title" && $fixuptype != "") $titles[] = array($id,$fixuptype);
			elseif ($type == "comment" && $fixuptype != "") $comments[] = array($id,$fixuptype);
		}
	}

	if ($start_callback) $start_callback(sizeof($posts) + sizeof($titles) + sizeof($comments));

	foreach ($titles as $t) {
		list ($id,$fixuptype) = $t;

		if ($progress_callback) $progress_callback($id);

		if (user_can_edit_post($user_ID, $id)) {
			$stuff = replacecrlf($wpdb->get_var("select post_title from {$wpdb->posts} where ID = $id LIMIT 1"));
			if ($fixuptype == "xmllint") {
				list($output,$suggestion) = run_content_through_xmllint_if_available($stuff);
			} elseif ($fixuptype == "tidy") {
				list($output,$suggestion) = run_content_through_tidy_if_available($stuff);
			} else { trigger_error("Unknown fixup type $fixuptype, bailing",E_USER_ERROR); }
			if ($suggestion) {
				$suggestion = $wpdb->escape($suggestion);
				$wpdb->query("UPDATE {$wpdb->posts} set post_title = '$suggestion' where ID = $id");
			}
		}
	}

	foreach ($posts as $t) {
		list ($id,$fixuptype) = $t;

		if ($progress_callback) $progress_callback($id);

		if (user_can_edit_post($user_ID, $id)) {
			$stuff = replacecrlf($wpdb->get_var("select post_content from {$wpdb->posts} where ID = $id LIMIT 1"));
			if ($fixuptype == "xmllint") {
				list($output,$suggestion) = run_content_through_xmllint_if_available($stuff);
			} elseif ($fixuptype == "tidy") {
				list($output,$suggestion) = run_content_through_tidy_if_available($stuff);
			} else { trigger_error("Unknown fixup type $fixuptype, bailing",E_USER_ERROR); }
			if ($suggestion) {
				$suggestion = $wpdb->escape($suggestion);
				$wpdb->query("UPDATE {$wpdb->posts} set post_content = '$suggestion' where ID = $id");
			}
		}
	}

	foreach ($comments as $t) {
		list ($id,$fixuptype) = $t;

		if ($progress_callback) $progress_callback($id);
		$postid = validator_get_post_id_from_comment_id($id);
		if (user_can_edit_post_comments($user_ID, $postid)) {
			$stuff = replacecrlf($wpdb->get_var("SELECT comment_content from {$wpdb->comments} where comment_ID = $id LIMIT 1"));
			if ($fixuptype == "xmllint") {
				list($output,$suggestion) = run_content_through_xmllint_if_available($stuff);
			} elseif ($fixuptype == "tidy") {
				list($output,$suggestion) = run_content_through_tidy_if_available($stuff);
			} else { trigger_error("Unknown fixup type $fixuptype, bailing",E_USER_ERROR); }
			if ($suggestion) {
				$suggestion = $wpdb->escape($suggestion);
				$wpdb->query("UPDATE {$wpdb->comments} set comment_content = '$suggestion' where comment_ID = $id");
			}
		}
	}

	if ($finish_callback) $finish_callback();


}


function validation_management_page() {
?>
<div class="wrap">

<h2>Validate all content</h2>

<?php if (validator_user_requested_validation()) { ?>

<p id="slated4removal1">WordPress XHTML validator is validating your content.  Please wait until the process is completed.</p>

<?php } elseif (validator_user_requested_mass_fix()) { ?>

<p id="slated4removal1">WordPress XHTML validator is auto-fixing your content.  Please wait until the process is completed.</p>

<?php } else { ?>

<p>WordPress XHTML validator will validate all posts, pages and comments against the XHTML 1.0 standard.  This way, when your readers visit your pages, they will be served XHTML.</p>
<p>Note: This tool <em>does not validate</em> the markup issued by your templates.  Validation is limited to both the <em>post and comment content</em> saved in your blog database and <em>rendered post and comment content</em> after processing by any active plugins.</p>
<p>Your template needs to be <a href="http://validator.w3.org/">checked for XHTML compliance</a> independently (suggestion: after the XHTML validator has reported full compliance, check your home page and a few other pages).</p>
<p>Any active filter plugins which transform post content or comments may alter their validity (and your weblog's, as well).  Particularly problematic is the Markdown plugin, which tends to generate invalid XHTML out of valid text and XHTML.  If you find that your posts or comments contain valid XHTML or pure text, and you still get validation errors, try to reformat posts and comments until you get good results, or disable your filter plugins one at a time to find the source of the problem.</p>

<?php } ?>

<p id="slated4removal2">Once the process is finished, you will see a report.  If the process aborts unexpectedly, please increase the maximum execution time (<code>max_execution_time</code> directive) and the memory limit (<code>memory_limit</code> directive) in your PHP configuration (<code>php.ini</code> or <code>.htaccess</code>, whichever you prefer).</p>

<?php if (validator_user_requested_validation()) { ?>
	<?php 
		$report = validator_mass_validate("validator_report_start","validator_report_finish","validator_report_progress");
	?>
	<script language="javascript" type="text/javascript">
		var d = document.getElementById("slated4removal1"); d.parentNode.removeChild(d);
		d = document.getElementById("slated4removal2"); d.parentNode.removeChild(d);
	</script>
	<p>The process is finished.  Here's a report of what WordPress XHTML validator found.</p>
	<?php validator_format_mass_report($report); ?>
	<?php if (!$report) { ?>
		<p>The contents of your weblog are fully XHTML compliant now.  Check your home page and a few other pages in your blog with the <a href="http://validator.w3.org/">W3C validator</a> to ensure that your template is XHTML compliant, and don't forget to join the <a href="http://rudd-o.com/projects/wp-validator/campaign/"><em>Truly Valid</em></a> campaign today!</p>
	<?php } ?>

<?php } elseif (validator_user_requested_mass_fix()) { ?>
	<?php 
		$report = validator_mass_fix("validator_report_start","validator_report_finish","validator_report_progress");
	?>
	<script language="javascript" type="text/javascript">
		var d = document.getElementById("slated4removal1"); d.parentNode.removeChild(d);
		d = document.getElementById("slated4removal2"); d.parentNode.removeChild(d);
	</script>
	<p>The process is finished.  Please run the validation process once again to see the report.</p>

<?php } ?>

<form action="" method="POST"><p class="submit"><input type="submit" name="xhtmlvalidate" value="Validate my content now"/></p></form>

</div>

<div class="wrap">

<h2>WordPress XHTML validator</h2>

<p class="credit">Created by <a href="http://rudd-o.com/">Rudd-O</a>.  Get the latest version at <a href="http://rudd-o.com/projects/wp-validator/">the WordPress XHTML validator Web site</a>.</p>

</div>



<?php } // end validation_management_page ?><?php

/* start dependency checkers */

function validator_notactive_warning() {
	autolink_display_warning("autolink-notactive","submenu","WordPress XHTML validator is not active",'Your Web server lacks the <code>xmllint</code> and the <code>tidy</code> commands.  WordPress XHTML validator needs at least one of them to perform its duties.');
}

function validator_deps_satisfied() {
	if (!test_for_xmllint() && !test_for_tidy()) return false;
	return true;
}

function validator_check_deps_and_warn() {
	if (!validator_deps_satisfied()) validator_notactive_warning();
}

add_action('admin_head','validator_check_deps_and_warn');

/* end dependency checkers */

/* start setup */
function validation_add_menu() {
	add_management_page('XHTML validator', 'XHTML validator', 1, __FILE__, 'validation_management_page');
}

if (validator_deps_satisfied()) {
	add_action('admin_menu', 'validation_add_menu');
	add_filter('admin_head', 'validation_validate_post_if_appropriate');
}

/* end setup */

?>
