/** * Converts the given XML tree and the given XSL file to a (HTML) file, with respect to the given XSL parameters. * The given XMLTree parameter may be of the SimpleXMLElement or the DOMDocument type, however, the latter is the fastest. * The generated output may be considered to be formatted properly. * @param mixed The XML Tree as a SimpleXMLElement or a DOMDocument * @param mixed The fullpath to the XSL file, or an instanced \SimpleXMLElement XSL tree * @param \System\Collection\Map The map with the parameters, or null for no parameters or default */ public final function render() { $args = func_get_args(); if (count($args) != 3) { throw new \InvalidArgumentException('Invalid amount of arguments given.'); } list($xmlTree, $xslFile, $parameters) = $args; $processor = new \XSLTProcessor(); //register the php functions if (count(self::$registeredPHPFunctions) > 0) { $processor->registerPHPFunctions(self::$registeredPHPFunctions); } //load the xsl file intro a dom $xsl = new \DOMDocument(); if ($xslFile instanceof \SimpleXMLElement) { $xsl->loadXML($xslFile->asXML()); } else { if (!file_exists($xslFile)) { throw new \System\Error\Exception\FileNotFoundException('XSL File: ' . $xslFile . ' cannot be found'); } $xsl->load($xslFile); } //attach the xsl dom to the processor $processor->importStylesheet($xsl); //when we run as a local debug system, we output the profiling information if (defined('DEBUG')) { $processor->setProfiling(PATH_LOGS . 'XSLTRenderProfiling.txt'); } $dom = new \DOMDocument(); switch (true) { case $xmlTree instanceof \SimpleXMLElement: //we need to convert to a domdocument $dom = \System\XML\XML::convertSimpleXMLElementToDOMDocument($xmlTree); break; case $xmlTree instanceof \DOMDocument: //no conversion needed break; default: throw new \InvalidArgumentException('Given XML tree is of non supported tree type: ' . get_class($xmlTree)); } $this->preprocessParameters($parameters); //we do not need any namespaces $processor->setParameter('', $parameters->getArrayCopy()); $output = $processor->transformToXML($dom); if (!$output) { throw new \Exception('Could not transform the given XML and XSL to a valid HTML page'); } if (MINIFY_ENABLE) { $output = \System\Web\Minify\HTML\Minify::minify($output); } $this->addToBuffer($output); }
/** * The main function of this object which simply renders the accumulated data using PHP's XSLTProcessor * @access public */ public function render() { try { $xsltproc = new \XSLTProcessor(); $xsltproc->importStylesheet($this->_styleSheet); if (isset($this->_profilerLogPath)) { $xsltproc->setProfiling($this->_profilerLogPath); } return $xsltproc->transformToXML($this->_inputData); } catch (Exception $e) { echo "Could not process the Datafile with the given Stylesheet: " . $e->getMessage(); } }
<?php $file = '/etc/passwd' . chr(0) . 'asdf'; $doc = new DOMDocument(); $proc = new XSLTProcessor(); var_dump($proc->setProfiling($file)); var_dump($proc->transformToURI($doc, $file));
require 'library/class.cli-configuration.php'; require 'library/functions.sanity-checks.php'; $Configuration = new CLIConfiguration(array('help::' => null, 'u=url:' => '', 'v=verbose' => 0, 'version' => null, 't=xsl:' => '', 'x=param:' => array(), 'p=preset:' => null)); $Configuration->SetFlagTriggers(array('help' => 'CLIVersion', 'verbose' => 'CLIVerbosity', 'param' => 'XSLParam')); $Configuration->AddPresetFlagTrigger(); $Collected = $Configuration->CollectArguments(); $Parameters = $Configuration->Param; $Url = $Configuration->Url; $Xsl = $Configuration->Xsl; // Sanity Checks SanityCheck(!empty($Url), 'No URL specified', 'CLIVersion'); SanityCheck(!empty($Xsl), 'No Transformation XSL file specified'); SanityCheck(file_exists($Xsl), '"' . $Xsl . '" does not exist'); libxml_use_internal_errors(true); $XsltProcessor = new XSLTProcessor(); $XsltProcessor->setProfiling('_output/profiling.txt'); $XmlString = ''; $XmlData = new DOMDocument('1.0', 'utf-8'); $XslData = new DOMDocument('1.0', 'utf-8'); $DomErrors = new DOMDocument('1.0', 'utf-8'); $XsltProcessor->setParameter('', 'request-url', $Url); if (is_array($Parameters)) { foreach ($Parameters as $Key => $Value) { $XsltProcessor->setParameter('', $Key, $Value); } } // Grab the remote XML file $C = curl_init($Url); curl_setopt_array($C, array(CURLOPT_USERAGENT => 'XSL-Transformation/' . VERSION . ' (like Feedburner)', CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_HEADER => false, CURLOPT_RETURNTRANSFER => true)); $XmlString = curl_exec($C); curl_close($C);
/** * проверяет возможности клиента и выдает как есть или выполняет трансформации сам * и выдает готовый html * @param string $data выходные данные в xml * @param boolean $html явное указание формата html * @param string $documentURI так как xml-документ $data передан строкой иногда требуется указать путь до него чтобы нашлись схемы * @param boolean $forceValidation NULL - валидация если в домашнем каталоге, TRUE: форсировать проверку по схеме/tidy всегда, FALSE - не проверять по схеме/tidy * @param string $htmlContentType миме-тип для вывода html, по-умолчанию text/html, для вывода html+xul передать application/xml */ public static function tryHTML($data, $html = FALSE, $documentURI = NULL, $forceValidation = NULL, $htmlContentType = "text/html") { $outputDom = NULL; $xsltResult = NULL; $debug = FALSE; $xsltProfiler = NULL; if (self::$done == TRUE) { trigger_error("Output::tryHTML() called twice?"); } $xsltStart = $xsltStop = 0; //минипрофайлер if ($html == FALSE && isset($_SERVER["HTTP_ACCEPT"])) { //тут пока неразбериха с text/xml по старому и application/xml по новому //опера по-новому, ие по-старому, мозиллы и так и сяк if (strpos($_SERVER["HTTP_ACCEPT"], "/xml") !== FALSE) { //тем кто явно грит "понимаю xml" сбросим флаг html $html = FALSE; if ("application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5" == $_SERVER["HTTP_ACCEPT"]) { //но некоторые говорят что понимают, но на самом деле не понимают, это андроиды 2.х их мы отлавливаем по кривому заголовку //под раздачу также попадают ближайщие родственники, которые реально умеют, но их не отличить от больных - сафари 4 (айфон 4). //остальные родственники успешно вылечились - сафари 5, хромы и хромиумы и другие вэбкитообразные. //используем заголовок Accept а не Usera-gent потому что //1. уже есть Vary: Accept - чтоб не плодить ветвление в прокси и кешах //2. у больных особей есть переключатель "mobile view" который влияет на User-agent-а // @seealso http://www.gethifi.com/blog/browser-rest-http-accept-headers // @seealso https://developer.mozilla.org/en-US/docs/HTTP/Content_negotiation $html = TRUE; } } elseif (strpos($_SERVER["HTTP_ACCEPT"], "text/html") !== FALSE) { //тем кто не понимает xml но явно грит что умеет html $html = TRUE; } } elseif (isset($_SERVER["HTTP_ACCEPT"])) { //"обратная автоматика" - даже если форсирован HTML но клиент его "не хочет" или "не может", но обещает что поймет XML - спрыгиваем на XML if (strpos($_SERVER["HTTP_ACCEPT"], "text/html") === FALSE && strpos($_SERVER["HTTP_ACCEPT"], "/xml") !== FALSE) { //тем кто явно заявляет что понимает xml и не умеет html $html = FALSE; } } //$html=TRUE; //подготовить стили для трансформации на php if ($debug || $html) { $outputDom = new \DOMDocument(); $outputDom->loadXML($data); if ($documentURI) { $outputDom->documentURI = $documentURI; } //валидация данных xml-схемой if ($debug) { $matches = NULL; //добываем имя схемы и проверяем по ней (или xmlreader?) if (preg_match("/schemaLocation=\".+\\s([a-zA-Z0-9_\\/\\.\\-]+)\"/", $data, $matches)) { $outputDom->schemaValidate(($documentURI ? dirname($documentURI) . "/" : "") . $matches[1]); // } else { // throw new \Exception("cant find schemaLocation"); } } $matches = NULL; //добываем имя стиля из хмл-а (или xmlreader?) if ($outputDom->firstChild->nodeType == XML_PI_NODE && $outputDom->firstChild->target == "xml-stylesheet") { if (preg_match("/href\\s*=\\s*\"(.+)\"/", $outputDom->firstChild->data, $matches)) { $oldHeaders = headers_list(); //время трансформации считаем общее - вместе с загрузкой документов $xsltStart = microtime(TRUE); $xsl = new \DomDocument(); // надо взять таблицу стилей локально $xsl_file = dirname($_SERVER["SCRIPT_FILENAME"]) . "/" . str_replace("http://" . $_SERVER["HTTP_HOST"] . dirname($_SERVER["PHP_SELF"]), "", $matches[1]); //error_log(dirname($_SERVER["SCRIPT_FILENAME"])); //error_log($_SERVER["PHP_SELF"]); //$xsl->load($matches[1]); $xsl->load("{$xsl_file}"); $proc = new \XSLTProcessor(); if ($xsltProfiler) { $proc->setProfiling($xsltProfiler); } $proc->importStyleSheet($xsl); //регистрируем на себя обращения к файлам stream_wrapper_unregister("file") or die(__FILE__ . __LINE__); stream_wrapper_register("file", static::CLASSNAME) or die(__FILE__ . __LINE__); //вешаем на обработчик выхода ловушку - если вложенный скрипт попытается сделать exit или die register_shutdown_function(array(static::CLASSNAME, "checkDone")); //на время трансформации ставим свой специальный обработчик ошибок set_error_handler(array(static::CLASSNAME, "xsltErrorHandler")); $xsltResult = $proc->transformToXML($outputDom); restore_error_handler(); if (self::$xsltErrors != NULL) { //а сообщаем об ошибках как обычно trigger_error("XSLTProcessor::transformToXml(): " . self::$xsltErrors); } //ставим маркер что управление нам вернули self::$done = TRUE; unset($proc, $xsl); //восстанавливаем дефолтный streamwrapper для file:// stream_wrapper_restore("file") or die(__FILE__ . __LINE__); //закончили трансформацию $xsltStop = microtime(TRUE); if ($xsltProfiler) { //ничего секретного там нет - даем всем почитать chmod($xsltProfiler, 0644); } //сравним хедеры до и после $diffHeaders = array_diff(headers_list(), $oldHeaders); //сбрасываем все хедеры которых "тут не стояло" foreach ($diffHeaders as $h) { $matches = explode(":", $h); header_remove($matches[0]); } //сбросим текущие чтобы добавить старые foreach ($oldHeaders as $h) { $matches = explode(":", $h); header_remove($matches[0]); } //востановим старые как были foreach ($oldHeaders as $h) { header($h, FALSE); } unset($diffHeaders, $oldHeaders, $h); } } else { //стиль не найден - html не получится - сбрасываем флаг $html = FALSE; } } self::$done = TRUE; //валидация выходного html с помощью tidy и по dtd-схеме if (1 != 1 && $debug && $xsltResult) { //http://dab.net.ilb.ru/doc/htmltidy-5.10.26-r2/html/quickref.html if (strncmp($xsltResult, "<!DOCTYPE html SYSTEM \"about:legacy-compat\">", 44)) { $config = array("output-xhtml" => TRUE, "doctype" => "strict"); $tidy = tidy_parse_string($xsltResult, $config, "UTF8"); //для tidy БЕЗ минуса $tidy->diagnose(); if (tidy_error_count($tidy) + tidy_warning_count($tidy)) { // tidy возвращает строку с ошибкой, пронумеровал для облегчения отладки $xsltResultLines = explode(PHP_EOL, $xsltResult); $xsltResultWithLines = ""; foreach ($xsltResultLines as $lineNumber => $line) { $xsltResultWithLines .= sprintf("%04d: ", $lineNumber + 1) . $line . PHP_EOL; } throw new Exception("tidy validation errors: " . $tidy->errorBuffer . PHP_EOL . $xsltResultWithLines); } //уберемся за собой unset($tidy, $config); } else { //html5 проверяем через локально развернутый сервис validator.nu $ch = curl_init(); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_VERBOSE, 0); curl_setopt($ch, CURLOPT_HEADER, 0); //TODO разобраться с нумерацией строк (с формы строки соответсвуют исходнику, а когда через сервис все в одну строку) curl_setopt($ch, CURLOPT_URL, "http://devel.net.ilb.ru:8888/?parser=html5&out=gnu"); curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: text/html")); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $xsltResult); ob_start(); curl_exec($ch); $out = ob_get_contents(); ob_end_clean(); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($code != 200) { trigger_error("curl failed " . curl_error($ch) . " HTTP" . $code . PHP_EOL . $out); } curl_close($ch); //исправление косяков html5-валидатора связанных с отсутствием стандарта //пропускаем предупреждения связанные с input type="date" $out = trim(preg_replace("/^:.+info warning: The “date” input type is so far supported properly.*/m", "", $out)); if ($out) { $xsltResultLines = explode(PHP_EOL, $xsltResult); $xsltResultWithLines = ""; foreach ($xsltResultLines as $lineNumber => $line) { $xsltResultWithLines .= sprintf("%04d: ", $lineNumber + 1) . $line . PHP_EOL; } throw new \Exception("validator.nu errors: " . PHP_EOL . $out . PHP_EOL . $xsltResultWithLines); } //уберемся за собой unset($ch, $out, $code); } $xsltResultDoc = new \DOMDocument(); //поиск схемы для проверки xhtml - ищем по обычным путям как и классы $schemasPath = NULL; foreach (explode(PATH_SEPARATOR, get_include_path()) as $p) { $p = $p . DIRECTORY_SEPARATOR . "schemas"; if (file_exists($p . DIRECTORY_SEPARATOR . "xhtml" . DIRECTORY_SEPARATOR . "dtd" . DIRECTORY_SEPARATOR . "xhtml1-strict.dtd")) { $schemasPath = $p; break; } } $xsltResultXml = str_replace(array("http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd", "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"), array($schemasPath . DIRECTORY_SEPARATOR . "xhtml" . DIRECTORY_SEPARATOR . "dtd" . DIRECTORY_SEPARATOR . "xhtml1-strict.dtd", $schemasPath . DIRECTORY_SEPARATOR . "xhtml11" . DIRECTORY_SEPARATOR . "dtd" . DIRECTORY_SEPARATOR . "xhtml11-flat.dtd"), $xsltResult); //поддерживается только Strict. как явно указать валидатору xml-catalog непонятно (с комстроки работает xmllint --catalog) $xsltResultDoc->loadXML($xsltResultXml); if ($documentURI) { $outputDom->documentURI = $documentURI; } if ($xsltResultDoc->doctype->systemId != "about:legacy-compat") { if (!$xsltResultDoc->validate()) { throw new \Exception("DTD validation errors"); } } unset($xsltResultDoc, $xsltResultXml, $schemasPath, $p); //тут можно и прочую статистику по внутренностям добавить header("X-XML-Output-tryHTML: HTML=" . ($html ? "TRUE" : "FALSE") . " XSLTtime=" . sprintf("%0.4f", $xsltStop - $xsltStart) . "s size=" . mb_strlen($data, "ASCII") . "/" . mb_strlen($xsltResult, "ASCII")); } unset($outputDom); //сообщаем клиенту что контент различен в зависимости от заголовка Accept - для кэширования нужно header("Vary: Accept"); if ($html) { unset($data); header("Content-type: {$htmlContentType};charset=UTF-8"); //header("Content-Length: ".mb_strlen($xsltResult,"ASCII")); //результат - сборная солянка из вывода нескольких скриптов и шаблонов //явно кешированием управлять не пытаемся: ставим хедеры на текущее время //TODO возможно и хитро проанализировать хедеры всех составляющих и вычислить общий //header("Last-Modified: ".gmdate(DATE_RFC1123)); //header("Etag: Output_".$_SERVER["UNIQUE_ID"]); echo $xsltResult; } else { unset($xsltResult); header("Content-type: application/xml"); //header("Content-Length: ".mb_strlen($data,"ASCII")); echo $data; } }