<?php
/*
Plugin Name: WordPress XHTML validator
Plugin URI: http://rudd-o.com/projects/wp-validator/
Description: Validates posts and pages as you edit them, and your entire blog on command.  To use, activate this plugin, go to the Manage panel, and then go to the XHTML validator subpanel.  Don't forget to <a href="http://rudd-o.com/projects/wp-validator/campaign/">join the Truly Valid campaign</a> today!
Version: 0.1.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
*/

//  DISABLED
// we sometimes gets called outside the WP context. This include makes it work
//
// if (!$wpdb) {
// 	require_once("../../wp-config.php");
// 	$validate_standalone = 1;
// }


define('v_testing_limit_post', false);
define('v_testing_limit_comment', false);
define('v_simulate_invalid_content', false);
define('v_simulate_filter_fuckup', false);
define('v_sleep_time_per_query_microseconds', 0); // zero microseconds


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 */

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

function dr($array) {
	print "<pre>";
	print_r($array);
	die;
}

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

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

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

function filter_content($content,$filtername) {
	if (v_simulate_filter_fuckup) $damage = "<shit";  // debug code: make this go bad!
	else $damage = "";
	return apply_filters($filtername, str_replace(']]>', ']]&gt;', $content)) . $damage;
}

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

function xmllint_output($output) {
	print "\n<pre style='height: 8em; overflow: auto;'>";
	print hc(rtrim($output));
	print "</pre>\n";
}


function textarea_output($output) {
	$height= '8em';
	print "\n<textarea style='width: 100%; height: {$height};'>";
	print hc($output);
	print "</textarea>\n";
}

function input_output($output) {
	print "\n<input style='width: 100%;' type='text' value='".hc($output)."' />";
}

function protected_output($output,$mode) {
	if ($mode == "input") input_output($output);
	elseif ($mode == "textarea") textarea_output($output);
}


function original_output($output,$mode='textarea') {
	protected_output($output,$mode);
}

function xmllint_suggestion_output($output,$mode='textarea') {
	protected_output($output,$mode);
}


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

function validation_add_menu() {

	$bstat_userlevel = 1;
	add_management_page('XHTML validator', 'XHTML validator', 1, __FILE__, 'validation_management_page');
}


/* VALIDATION CORE */

define('ORIGINAL_CONTENT_INVALID',1);
define("FILTER_FUCKUP",2);

class InvalidEntry {
	function InvalidEntry($id,$content,$validation_output,$correct_html,$reason,$filtered_content) {
		$this->id = $id;
		$this->content = $content;
		$this->validation_output = $validation_output;
		$this->correct_html = $correct_html;
		$this->reason = $reason;
		$this->filtered_content = $filtered_content;
	}
}

class DummyPost {
	function DummyPost ($id) {
		$this->ID = $id;
	}
}

class DummyComment {
	function DummyComment ($id) {
		$this->comment_ID = $id;
	}
}

$contentfilename = null;
$contentfile = null;

function validate_content($id,$content) {
	global $validate_encoding;

	$cmd = "nice xmllint --html --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 = 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];
	}
// 	else {
// 		die("<pre>".hc($stdout)."could not find the shit");
// 	}
	$stdout = str_replace("<!--","\n\n<!--",$stdout);
	$stdout = str_replace("-->","-->\n\n",$stdout);

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

function validate_by_query($query,$filtername,$callback) {
	global $wpdb;
	global $validate_header,$validate_footer;

	$invalid_entries = array();
	$results = $wpdb->get_results($query);
	foreach ($results as $result) {
		$callback($result->ID);
		if (v_simulate_invalid_content) $result->content .= "<shit";  // debug code: make this go bad!
		
		// the database extraneously sends them with crlf - we only take CRs because we are unix
		$content = replacecrlf($result->content);

		// first, a validation without filters
		$r = validate_content($result->ID,$content);
		$validate_post_result = $r[0]; $correct_html = $r[1];

		if ($validate_post_result) {
			$invalid_entries[] =
				new InvalidEntry($result->ID,$content,
				$validate_post_result,$correct_html,ORIGINAL_CONTENT_INVALID,"");
		} else {
			// now, if that was fine, a second validation with filters enabled,
			// to see if they produce invalid HTML out of the entry text
			// this time if we find invalid XHTML we also report the filtered content
			$filtered_content = filter_content($content,$filtername);
			$r = validate_content($result->ID,$filtered_content);
			$validate_post_result = $r[0]; $correct_html = $r[1];

			if ($validate_post_result)
				$invalid_entries[] =
					new InvalidEntry($result->ID,$content,
					$validate_post_result,$correct_html,FILTER_FUCKUP,$filtered_content);
		}
		if (v_sleep_time_per_query_microseconds) usleep(v_sleep_time_per_query_microseconds);
	}
	return $invalid_entries;
}

function validate_single_post_callback() {
	return;
	//does nothing
}

function validate_single_post($id) {
	global $wpdb;
	$filtername = 'the_content';
	return validate_by_query(
		"SELECT ID,post_content as content from $wpdb->posts where ID = $id;",
			$filtername,validate_single_post_callback);
}

function validate_single_title_callback() {
	return;
	//does nothing
}

function validate_single_title($id) {
	global $wpdb;
	$filtername = 'the_content';
	return validate_by_query(
		"SELECT ID,post_title as content from $wpdb->posts where ID = $id;",
			$filtername,validate_single_title_callback);
}

function validate_posts($callback) {
	global $wpdb;
	if (v_testing_limit_post) $limit = "limit ". v_testing_limit_post;
	$filtername = 'the_content';
	return validate_by_query("SELECT ID,post_content as content from $wpdb->posts order by ID $limit;",$filtername,$callback);
}

function validate_titles($callback) {
	global $wpdb;
	if (v_testing_limit_post) $limit = "limit ". v_testing_limit_post;
	$filtername = 'the_title';
	return validate_by_query("SELECT ID,post_title as content from $wpdb->posts order by ID $limit;",$filtername,$callback);
}

function validate_comments($callback) {
	global $wpdb;
	if (v_testing_limit_comment) $limit = "limit ". v_testing_limit_comment;
	$filtername = 'comment_text';
	return validate_by_query("SELECT comment_ID as ID,comment_content as content from $wpdb->comments where comment_approved != 'spam' order by ID $limit;",$filtername,$callback);
}

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

function validation_on_edit_post() {

	?>
	<script>
	function addToElement(elementname,astring,styleinfo) {
		var postdiv = document.getElementById(elementname);
		var containernode = document.createElement("p");
		var containernode2 = document.createElement("em");
		containernode.setAttribute("class","updated");
		containernode.setAttribute("style", "text-align: center; " + styleinfo);
		containernode2.appendChild(document.createTextNode(astring));
		containernode.appendChild(containernode2);
		postdiv.insertBefore(containernode,postdiv.childNodes[0]);
	}
	</script>
	<?php

	function addToElement($elementname,$astring,$styleinfo) {
		print '<script>';
		print "addToElement('$elementname','$astring','$styleinfo');"; // ADDSLASHES??? FIXME
		print '</script>';
	}

	if (!test_for_xmllint()) {
		addToElement("postdiv","Cannot validate this entry.  Your Web hosting server does not have xmllint installed.",';');
		return; /* we bail because there is no point in going on */
	}


	$validationresult = validate_single_title(intval($_GET["post"]));
	if (!$validationresult) {
// 		showBelowTextarea("This entry contains and renders as valid XHTML");
	} else {
		$ientry = $validationresult[0];
	
		if ($ientry->reason == ORIGINAL_CONTENT_INVALID) $reason = "contains invalid XHTML.  Please fix it.";
		elseif ($ientry->reason == FILTER_FUCKUP) $reason = "is being rendered invalid by an active filter plugin &mdash; the page for this entry will contain invalid XHTML.  Try deactivating your filter plugins one by one to find the source of the problem.";
		$errorstring = "This title $reason";
	
		addToElement("titlediv",$errorstring,';');
	}

	$validationresult = validate_single_post(intval($_GET["post"]));
	if (!$validationresult) {
// 		showBelowTextarea("This entry contains and renders as valid XHTML");
	} else {
		$ientry = $validationresult[0];
	
		if ($ientry->reason == ORIGINAL_CONTENT_INVALID) $reason = "contains invalid XHTML.  Please fix it.";
		elseif ($ientry->reason == FILTER_FUCKUP) $reason = "is being rendered invalid by an active filter plugin &mdash; the page for this entry will contain invalid XHTML.  Try reformatting the whitespace or deactivating your filter plugins one by one to find the source of the problem.";
		$errorstring = "This entry $reason";
	
		addToElement("postdiv",$errorstring,';');
	}

// 	dr($post);
}

function validation_on_post_page() {
	if ($_GET["action"] == "edit" && intval($_GET["post"])) {
		validation_on_edit_post();
	}
}

function validation_admin_dispatcher() {
	if(strpos($_SERVER['REQUEST_URI'], 'post.php'))
	{
		validation_on_post_page();
	}
}



/* REPORTING FUNCTIONS used in the admin interface */

// init vars for reporting functions
$progress_counter = 0;
$validation_total_count = 0;
$report_interval = 0;
function report_general_progress($id) {
	global $progress_counter;
	global $validation_total_count;
	global $report_interval;
	$text = "Validating entry $progress_counter (out of a total $validation_total_count entries)...";
	$progress_counter = $progress_counter + 1;
	if (($progress_counter % $report_interval == 0)) {
		print "\n";
		print '<script>';
		$intext = intval(intval($progress_counter)/intval($validation_total_count)*100);
		print "us($progress_counter,$validation_total_count,'$intext');";
		print '</script>';
		ob_flush();
		flush();
	}
}

function report_validation_start() {
	global $progress_counter;
	global $validation_total_count;
	global $report_interval;
	print "\n";
	print '<script>';
	print "us($progress_counter,$validation_total_count,'0');";
	print '</script>';
	ob_flush();
	flush();
}

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

function validation_css() {

print "<style type='text/css'>";

print '
#validation-progress-bar-container {
border: 1px solid #A3A3A3;
background:#F2F2F2;
}

#validation-progress-bar {
border: 1px solid #A3A3A3;
background: #A3A3A3;
height: 1em;
}
';

print "</style>";

}

//
// 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 validation_management_page() {
?>
<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>

<?php

	if (!test_for_xmllint()) {

?>

<p>Your Web hosting server does not have <code>xmllint</code> installed.  Please install it or have the administrator install it for you, then try again.</p>

<?php
	
	} else {

?>

<script language="JavaScript" type="text/javascript">
function updateStatusBar(newStatusText) {
	var vsnode = document.getElementById("validation-progress-bar");
	var style = "width: " + newStatusText + "%;";
	vsnode.setAttribute("style",style);
}
function us(progress,total,intext) {
	var vsnode = document.getElementById("validation-status");
	while (vsnode.childNodes.length > 0) vsnode.removeChild(vsnode.childNodes[0]);
	vsnode.appendChild(document.createTextNode("Validating entry " + progress + " out of " + total + " entries..."));
	updateStatusBar(intext);
}
function deleteStatus() {
	vsnode = document.getElementById("validation-progress-bar-container");
	vsnode.parentNode.removeChild(vsnode);
	vsnode = document.getElementById("validation-help");
	vsnode.parentNode.removeChild(vsnode);
	var vsnode = document.getElementById("validation-status");
	vsnode.parentNode.parentNode.removeChild(vsnode.parentNode);
}
</script>

<div id="validation-help">
<p>Note: This tool <em>does not validate</em> pages in your blog.  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>
</div>

<p>If the validation process aborts unexpectedly, please increase the maximum execution time (<code>max_execution_time</code> directive) in your PHP configuration (<code>php.ini</code>).</p>

<h4 style="text-align:center; margin-bottom: 0.1em; padding-bottom: 0em;"><span id="validation-status">Validation initiating...</span></h4><div id="validation-progress-bar-container"><div id="validation-progress-bar" style="display: none;"></div></div>

<?php

		global $validation_total_count;
		global $report_interval;
		global $wpdb;
		$total_posts = v_testing_limit_post ? v_testing_limit_post : $wpdb->get_var("select count(ID) from $wpdb->posts;");
		$total_comments = v_testing_limit_comment ? v_testing_limit_comment : $wpdb->get_var("select count(comment_ID) from $wpdb->comments where comment_approved != 'spam' ;");
		$validation_total_count = $total_posts * 2 + $total_comments;
		$report_interval = intval($validation_total_count/100);
		if ($report_interval < 1) $report_interval = 1;
		if ($report_interval > 50) $report_interval = 50;

		report_validation_start();

		$invalidtitles = validate_titles(report_general_progress);
		$invalidposts = validate_posts(report_general_progress);
		$invalidcomments = validate_comments(report_general_progress);
		report_validation_finish();

?>
</div>
<div class="wrap">
<h2>Validation results</h2>
<?php

		if ((!$invalidtitles) && (!$invalidposts) && (!$invalidcomments)) {
			print '<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>';
		} else {

			if ($invalidtitles) {
				foreach($invalidtitles as $ientry) {
					global $post;
					$post = new DummyPost($ientry->id);
					if ($ientry->reason == ORIGINAL_CONTENT_INVALID) $reason = "contains invalid XHTML";
					elseif ($ientry->reason == FILTER_FUCKUP) $reason = "is being rendered invalid by an active filter plugin";
					print "<p>"; print "The title of post or page $ientry->id "; edit_post_link("(edit)"); print " $reason. <code>xmllint</code> output follows:</p>";
					xmllint_output($ientry->validation_output);
					print "<p>";
					print "This is the original content:<br/>";
					original_output($ientry->content,$mode="input");
					print "</p>";
					$reference = "using the original content as a starting point";
					if ($ientry->reason == FILTER_FUCKUP) {
						$reference = "using the filtered content as starting point";
						print "<p>";
						print "This is the content after being filtered by your plugins:<br/>";
						original_output($ientry->filtered_content,$mode="input");
						print "</p>";
					}
					print "<p>";
					print "<code>xmllint</code> makes the following suggestion, $reference:<br/>";
					xmllint_suggestion_output($ientry->correct_html,$mode="input");
					print "</p>";
					print "<hr/>";
				}
			} else {
				print "<p>All post and page titles contain valid XHTML.</p>";
			}
			if ($invalidposts) {
				foreach($invalidposts as $ientry) {
					global $post;
					$post = new DummyPost($ientry->id);
					if ($ientry->reason == ORIGINAL_CONTENT_INVALID) $reason = "contains invalid XHTML";
					elseif ($ientry->reason == FILTER_FUCKUP) $reason = "is being rendered invalid by an active plugin";
					print "<p>"; print "Post or page $ientry->id "; edit_post_link("(edit)"); print " $reason. <code>xmllint</code> output follows:</p>";
					xmllint_output($ientry->validation_output);
					print "<p>";
					print "This is the original content:<br/>";
					original_output($ientry->content);
					print "</p>";
					$reference = "using the original content as a starting point";
					if ($ientry->reason == FILTER_FUCKUP) {
						$reference = "using the filtered content as starting point";
						print "<p>";
						print "This is the content after being filtered by your plugins:<br/>";
						original_output($ientry->filtered_content);
						print "</p>";
					}
					print "<p>";
					print "<code>xmllint</code> makes the following suggestion, $reference:<br/>";
					xmllint_suggestion_output($ientry->correct_html);
					print "</p>";
					print "<hr/>";
				}
			} else {
				print "<p>All posts and pages contain valid XHTML.</p>";
			}
			if ($invalidcomments) {
				foreach($invalidcomments as $ientry) {
					global $post,$comment;
					$post = new DummyPost(get_post_id($ientry->id));
					$comment = new DummyComment($ientry->id);
					if ($ientry->reason == ORIGINAL_CONTENT_INVALID) $reason = "contains invalid XHTML";
					elseif ($ientry->reason == FILTER_FUCKUP) $reason = "is being rendered invalid by an active plugin";
					print "<p>"; print "Comment $ientry->id "; edit_comment_link("(edit)"); print " $reason. <code>xmllint</code> output follows:</p>";
					xmllint_output($ientry->validation_output);
					print "<p>";
					print "This is the original content:<br/>";
					original_output($ientry->content);
					print "</p>";
					$reference = "using the original content as a starting point";
					if ($ientry->reason == FILTER_FUCKUP) {
						$reference = "using the filtered content as starting point";
						print "<p>";
						print "This is the content after being filtered by your plugins:<br/>";
						original_output($ientry->filtered_content);
						print "</p>";
					}
					print "<p>";
					print "<code>xmllint</code> makes the following suggestion, $reference:<br/>";
					xmllint_suggestion_output($ientry->correct_html);
					print "</p>";
					print "<hr/>";
				}
			} else {
				print "<p>All comments contain valid XHTML.</p>";
			}
		}
	}
?>
</div>
<?php } // end validation_management_page ?><?php add_action('admin_menu', 'validation_add_menu'); add_filter('admin_footer', 'validation_admin_dispatcher');add_filter('admin_head', 'validation_css'); //FIXME there has to be a better way to do this than just the admin_footer callback!!!!
?>