<?php
/*

This is the TagMogrifier library for PHP
Created by Rudd-O
Version: 0.2.0
Distributed under the GPL

The TagMogrifier library lets you perform quick and simple transformations on HTML, XHTML and XML, with a minimum of fuss.  TagMogrifier requires the PHP DOM extension.

How do you use this? Simple.  Example:

Say you have an XML document in the variable $xmlstr, and you want to add a mogrified='true' attribute to all tags that have an href attribute.  You just do this:

function addattribute(&$tag) {
	$tag->setAttribute("mogrified","true");
}
$m = new TagMogrifier();
$m->add_tag_match(null,"string:href",null);
$a->add_tag_mogrificator("addattribute");
print $a->run($xmlstr);

You can also add mogrifications for attributes.

To learn more, see the README, the function documentation for the TagMogrifier class, and the DOM extension documentation, for information on how to perform transformations on DOM nodes.

*/


/*function dumpassoc($list) {
	foreach ($list as $l=>$n) print $l . " => " . $n. "\n";
}
function dumpkeys($list) {
	foreach ($list as $l=>$n) print $l . "\n";
}
function dumpvals($list) {
	foreach ($list as $l) print $l . "\n";
}

*/

function tagmogrify_getnodetype($nodetype) {
$nodetypes = array(
XML_ELEMENT_NODE=>"XML_ELEMENT_NODE"
,XML_ATTRIBUTE_NODE=>"XML_ATTRIBUTE_NODE"
,XML_TEXT_NODE=>"XML_TEXT_NODE"
,XML_CDATA_SECTION_NODE=>"XML_CDATA_SECTION_NODE"
,XML_ENTITY_REF_NODE=>"XML_ENTITY_REF_NODE"
,XML_ENTITY_NODE=>"XML_ENTITY_NODE"
,XML_PI_NODE=>"XML_PI_NODE"
,XML_COMMENT_NODE=>"XML_COMMENT_NODE"
,XML_DOCUMENT_NODE=>"XML_DOCUMENT_NODE"
,XML_DOCUMENT_TYPE_NODE=>"XML_DOCUMENT_TYPE_NODE"
,XML_DOCUMENT_FRAG_NODE=>"XML_DOCUMENT_FRAG_NODE"
,XML_NOTATION_NODE=>"XML_NOTATION_NODE"
,XML_HTML_DOCUMENT_NODE=>"XML_HTML_DOCUMENT_NODE"
,XML_DTD_NODE=>"XML_DTD_NODE"
,XML_ELEMENT_DECL_NODE=>"XML_ELEMENT_DECL_NODE"
,XML_ATTRIBUTE_DECL_NODE=>"XML_ATTRIBUTE_DECL_NODE"
,XML_ENTITY_DECL_NODE=>"XML_ENTITY_DECL_NODE"
,XML_NAMESPACE_DECL_NODE=>"XML_NAMESPACE_DECL_NODE"
,XML_LOCAL_NAMESPACE=>"XML_LOCAL_NAMESPACE"
// ,XML_ATTRIBUTE_CDATA=>"XML_ATTRIBUTE_CDATA"
// ,XML_ATTRIBUTE_ID=>"XML_ATTRIBUTE_ID"
// ,XML_ATTRIBUTE_IDREF=>"XML_ATTRIBUTE_IDREF"
// ,XML_ATTRIBUTE_IDREFS=>"XML_ATTRIBUTE_IDREFS"
// ,XML_ATTRIBUTE_ENTITY=>"XML_ATTRIBUTE_ENTITY"
// ,XML_ATTRIBUTE_NMTOKEN=>"XML_ATTRIBUTE_NMTOKEN"
// ,XML_ATTRIBUTE_NMTOKENS=>"XML_ATTRIBUTE_NMTOKENS"
// ,XML_ATTRIBUTE_ENUMERATION=>"XML_ATTRIBUTE_ENUMERATION"
// ,XML_ATTRIBUTE_NOTATION=>"XML_ATTRIBUTE_NOTATION"
);

return $nodetypes[$nodetype];
}


class TagMogrifier {

	/* initialize with the encoding (mandatory) of the xml it needs to expect, whether it will fatal-bomb-out on errors or not, and an XML snippet with entity declarations, if required (HTML needs it for most HTML files to be parsed, see function autolink_get_entity_html() on the WordPress AutoLink plugin) */
	function TagMogrifier($encoding,$fatal_errors=true,$entity_declarations="") {
		$this->matches = array();
		$this->mogrificators = array();
		$this->attr_matches = array();
		$this->attr_mogrificators = array();
		$this->encoding = $encoding;
		$this->fatal_errors = $fatal_errors;
		$this->entity_declarations = $entity_declarations;
	}

	/*
	this function lets you specify which tags to mogrify, based on a Boolean AND (&&) of all specified conditions
	this function takes three parameters:
		an expression to match a tag name
		an expression to match an attribute of a tag
		an expression to match the value of a tag attribute
	each parameter to this function can be specified in the following way (examples):
		anything: null.  null always matches.
		an exact match on a string: "string:abcde" would match "abcde"
		a case-insensitive exact match on a string: "istring:abcde" would match "abCDe"
		a regular expression match: "regexp:/abcdefg/" would run PCRE using abcdefg as an expression
		a function match: "function:a_function_name" would invoke a_function_name passing a string, and if the function returns true, the match is considered positive
	
	Note that regexp expressions must be enclosed in the delimiter (customarily '/') and may contain modifiers.
	*/
	function add_tag_match($tagname_re=null,$attrname_re=null,$attrvalue_re=null) {
		$matches = array($tagname_re,$attrname_re,$attrvalue_re);
		foreach ($matches as $expr) $this->_getMatchTypeAndExpression($expr);
		$this->matches[] = $matches;
	}

	function add_attribute_match($tagname_re=null,$attrname_re=null,$attrvalue_re=null) {
		$matches = array($tagname_re,$attrname_re,$attrvalue_re);
		foreach ($matches as $expr) $this->_getMatchTypeAndExpression($expr);
		$this->attr_matches[] = $matches;
	}
	
	/*
	a tag mogrificator function is a function which receives only one parameter (and should receive it by reference, with an & in the function declaration of the parameter).  This function will be called each time a tag matches any of the matches specified with add_tag_match.

	on each call of the mogrificator function, it will receive a DOMElement instance (different each time) which the function can (and perhaps should) modify to achieve its goals (this is why the mogrificator function should be declared to receive this argument by reference)

	see the PHP documentation for information on the DOMElement class
	*/
	function add_tag_mogrificator($mogrification_function) {
		$this->mogrificators[] = $mogrification_function;
	}

	function add_attribute_mogrificator($mogrification_function) {
		$this->attr_mogrificators[] = $mogrification_function;
	}

	/*
	this function returns the result of the mogrification, reserialized, against the XML text you pass to it.
	if fatal errors was specified as true in the constructor (default), any errors during parsing are converted to fatal errors, otherwise, FALSE is returned from the processing.  If you run with fatal errors = false, you can silence any warnings produced by the run method by prepending an at sign like this: @$instance->run($xml); - in that case, don't forget to check for FALSE as a return value from the run() method.
	*/
	function run($xml) {
		if ($xml === false) {
			$error_level = E_USER_WARNING;
			if ($this->fatal_errors) $error_level = E_USER_ERROR;
			trigger_error("TagMogrify was supplied with a FALSE value instead of XML", $error_level);
			return false;
		}

		$mogrificationstring = "<mogrification>$xml</mogrification>";
		$encodingstring = "<?xml version=\"1.0\" encoding=\"".htmlspecialchars($this->encoding)."\"?>";
		$string = $encodingstring . $this->entity_declarations . $mogrificationstring;
		$sxml = simplexml_load_string($string);
		if ($sxml === false) {
			$error_level = E_USER_WARNING;
			if ($this->fatal_errors) $error_level = E_USER_ERROR;
			trigger_error("TagMogrify encountered invalid XML: " . htmlentities($xml), $error_level);
			return false;
		}
		$this->dom = dom_import_simplexml($sxml);
		$this->_recurseIntoDom();
		$sxml = simplexml_import_dom($this->dom);
		$newxml = $sxml->asXML();
		preg_match("|<mogrification>(.*)</mogrification>|s",$newxml,$matches);
		return $matches[1];
	}


	function _recurseIntoDom() {
		$basenode = $this->dom; // we begin with firstChild to not touch my bogus mogrification tag
		$this->_dealWithNode($basenode);
	}
	
	function _dealWithNode(&$node) { // depth-first traversal of the tree
		if ($node->hasChildNodes()) {
			foreach ($node->childNodes as $key=>$childnode) $this->_dealWithNode($childnode);
		}
		$this->_processThisNode($node);
	}
	
	function _processThisNode(&$node) {
// 		print "nodename: ".$node->nodeName."    node type: " . tagmogrify_getnodetype($node->nodeType) . "\n";
		if ($node->nodeType == XML_ELEMENT_NODE && $node->tagName === "mogrification") {
			// do nothing, dude, don't touch that node!
			return;
		}
		if ($node->nodeType == XML_ELEMENT_NODE) {
			$this->_processTag($node);
		}
		if ($node->hasAttributes()) {
			foreach($node->attributes as $attribute) {
				$this->_processAttribute($attribute);
			}
		}
	}

	function _getMatchTypeAndExpression($expr) {
		if ($expr === null) return array(null,null);
		$regexp = "/^(.+?):(.*)$/";
		if (preg_match($regexp,$expr,$matches)) {
			$type = $matches[1]; $match = $matches[2];
			$valid_types = array("string","istring","regexp","function");
			if (in_array($type,$valid_types)) return array($type,$match);
		}
		$error_level = E_USER_WARNING;
		if ($this->fatal_errors) $error_level = E_USER_ERROR;
		trigger_error("TagMogrify was supplied with an invalid match expression $expr", $error_level);
	}

	function _executeMatch($expr,$string) {
		list($type,$match) = $this->_getMatchTypeAndExpression($expr);
		switch ($type) {
			case "string": return ($match === $string);
			case "istring": return (strtolower($match) === strtolower($string));
			case "regexp": if (preg_match($match,$string)) return true; return false;
			case "function": if ($match($string)) return true; return false;
		}
		return false;
	}

	function _processTag($tag) {
		$did_match = false;

		foreach ($this->matches as $currmatch) {
			$tagmatch = $currmatch[0];
			$attrmatch = $currmatch[1];
			$valmatch = $currmatch[2];
			if ($tagmatch === null || $this->_executeMatch($tagmatch,$tag->tagName)) {
				if (!$tag->hasAttributes()) {
					if ($attrmatch === null && $valmatch === null) {
						$did_match = true;
					}
				} else foreach ($tag->attributes as $attr) {
					$attrname = $attr->nodeName;
					$attrvalue = $attr->nodeValue;
					if ($attrmatch === null || $this->_executeMatch($attrmatch,$attrname)) {
						if ($valmatch === null || $this->_executeMatch($valmatch,$attrvalue)) {
							$did_match = true;
						}
						if ($did_match) break;
					}
					if ($did_match) break;
				}
			}
			if ($did_match) break;
		}

// 		if ($did_match) print $tag->nodeName . " matched, mogrifying\n";
// 		if (!$did_match) print $tag->nodeName . " did not match\n";
		if ($did_match) { foreach ($this->mogrificators as $mog) { $mog($tag); } }
		
	}

	function _processAttribute($attribute) {
		$did_match = false;

		foreach ($this->attr_matches as $currmatch) {
			$tagmatch = $currmatch[0];
			$attrmatch = $currmatch[1];
			$valmatch = $currmatch[2];
			if ($tagmatch === null || $this->_executeMatch($tagmatch,$attribute->parentNode->tagName)) {
				if ($attrmatch === null && $valmatch === null) {
					$did_match = true;
				} else {
					$attrname = $attribute->nodeName;
					$attrvalue = $attribute->nodeValue;
					if ($attrmatch === null || $this->_executeMatch($attrmatch,$attrname)) {
						if ($valmatch === null || $this->_executeMatch($valmatch,$attrvalue)) {
							$did_match = true;
						}
					}
				}
			}
			if ($did_match) break;
		}

// 		if ($did_match) print "attribute " .$attribute->nodeName . " matched, mogrifying\n";
// 		if (!$did_match) print "attribute " .$attribute->nodeName . " did not match\n";
		if ($did_match) { foreach ($this->attr_mogrificators as $mog) { $mog($attribute); } }
		
	}

}

function testMogrify() {

$testxml = "<p>
<a href=\"http://www.plastic.com/article.pl?sid=01/09/08/1549202&amp;from=rdf\">Over here at Plastic</a> I just read about the latest offering from the U.S. Congress.  It's called the SSSCA, and it's brought to you by Sen. Fritz Hollings (for those of you informed about the <a href=\"http://www.anti-dmca.org\">DMCA</a>, you know who he is).
</p><p>
I'm not going to get into the details of what it is supposed to do and what it really does.  I'm just going to dissect the possible effects of the law:
</p>
<!--more-->

<ol>

<li>
It will make free software in (at least) the domain of operating systems illegal.<br/>
Yes, no more Linux.  Why? Because a DRM scheme like the proposal for the SSSCA requires operating system-level support.  And obviously, no self-respecting Linux advocate is going to write support for it.  16% of the global computer users will be left in the dark or forced to shell out the money for a license for an encumbered and government-approved operating system.<br/>
<i>Global? Isn't the law only valid in the States?</i> Think again: first, the U.S. tends to export policy, technology and law to other countries, especially third-world ones.  Second, most PC manufacturers (no matter their resistance to it) probably won't be able to afford to differentiate markets and will ship computers with the same restrictions to other parts of the globe.
</li><li>
It will make many DIY modifications to hardware illegal.<br/>
You might stumble on a modification you really need, and upon performing it you might accidentally or intentionally disable the DRM scheme.
</li><li>
It will close access to and sharing of many of your electronic documents.<br/>
As the tendency of the software industry goes, many a file format will be SSSCA-DRM enabled, and I bet you will have problems distributing your information or restoring it from backups.  Even if you don't have problems, DRM will make the task of restoring backups cumbersome.
</li><li>
It will make anonymous speech nearly impossible.<br/>
Since most of the file formats will be DRM-enabled, your information will be timestamped and associated with your personal creation ID.  That is, if the scheme enforces personal IDs, which are required for a workable and enforceable DRM solution.  So everywhere someone enjoys (or regrets) something you worked on, you can be sure he/she'll know you were involved.
</li><li>
It will outlaw many fair uses we know and enjoy.<br/>
Look at Dmitry Skylarov.  A tool for fair use enforcement is now deemed illegal.  Continuing this trend, we should begin jailing Smith &amp; Wesson employees (for the record and in interest of full disclosure, I am a strong advocate of people arming and defending themselves).
</li><li>
It will make unsanctioned content akin to pirate or outlaw content.<br/>
Perhaps the most chilling effect that the SSSCA will have on free speech is that unsanctioned (read: not DRM-licensed but freely authored or in free-to-use formats) speech will be seen by future generations as something to run away from, surrounded by an aura of forbidden and bad, behavior that surely will be instituted by megacorporations, such as Disney, that are sponsoring the SSSCA.  This can be expected, since the RIAA tried it with its \"Home taping is killing music\" propaganda campaign in the 1970's.  It's in every media mogul's agenda.
</li><li>
It will stifle innovation, create entry barriers for small inventors and ensure megacorporation technology control, and widen the digital divide.<br/>
The integration of the mandatory DRM technology will drive up the costs of hardware development, manufacture and distribution. This will ensure that low-capital startups will endure hard times, while guaranteeing market dominance for huge corporations who can afford to revamp their product lines.  The same problem will be experienced in smaller countries who will have a larger barrier of entry into the U.S. market.</li></ol>
<p>
I held some hope until today. Now I'm convinced that our grandchildren will enjoy the <a href=\"http://www.gnu.org/philosophy/right-to-read.html\">Right to read (TM)</a> future.
</p>
<p>This has been syndicated at <a href=\"http://www.osopinion.com/perl/story/13542.html\">osOpinion</a> and <a href=\"http://linuxtoday.com/news_story.php3?ltsn=2001-09-11-011-20-OP-HW-SW\">LinuxToday.</a></p>



<p>This document is licensed under the <a href=\"http://www.gnu.org/licenses/fdl.html\">GNU FDL</a></p>
";

function printas(&$tag) {
	print "Tag " . $tag->nodeName . "\n";
}

function printhrefs(&$tag) {
	print "Attribute " . $tag->nodeName . "=" . $tag->nodeValue . "\n";
}

// $m = new TagMogrifier("utf-8");
// $m->add_tag_match("string:a","string:href",null);
// $m->add_tag_mogrificator("printas");
// // die("shit\n".$testxml);
// $m->run($testxml);
// 
// print "\n\n";

$m = new TagMogrifier("utf-8");
// $m->add_attribute_match();
$m->add_attribute_match("string:a","string:href","regexp:|org|");
$m->add_attribute_mogrificator("printhrefs");
// die("shit\n".$testxml);
$m->run($testxml);

}

// testMogrify();

?>