Пример #1
0
<?php

require_once __DIR__ . '/../conf/bootstrap.php';
require_once __DIR__ . '/../conf/conf.php';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
    if (!isset($GLOBALS["HTTP_RAW_POST_DATA"])) {
        $GLOBALS["HTTP_RAW_POST_DATA"] = file_get_contents("php://input");
    }
    if (array_key_exists("CONTENT_TYPE", $_SERVER) && strpos($_SERVER["CONTENT_TYPE"], "/xml") !== FALSE) {
        $xmlstr = $GLOBALS["HTTP_RAW_POST_DATA"];
        $params = [];
        parse_str($_SERVER['QUERY_STRING'], $params);
        if (isset($params["xsltDocument"])) {
            $doc = new \DOMDocument();
            $xslt = $doc->createProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="stylesheets/Archive/' . $params["xsltDocument"] . '"');
            $doc->appendChild($xslt);
            $xml = new \DOMDocument();
            $xml->loadXML($xmlstr);
            $root = $xml->documentElement;
            $newRoot = $doc->importNode($root, true);
            $doc->appendChild($newRoot);
            \XML_Output::tryHTML($doc->saveXML(), true);
            exit(0);
        }
        throw new \Exception("Unsupported Media Type. */xml content type supported only.", 415);
    }
    throw new \Exception("Query param `xsltDocument` not found", 400);
}
throw new \Exception("Method not allowed", 405);
Пример #2
0
 /**
  * проверяет возможности клиента и выдает как есть или выполняет трансформации сам
  * и выдает готовый 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("XML_Output::tryHTML() called twice?");
     }
     //работаем только в контексте web - определяем местонахождение по url
     if ($_SERVER["PHP_SELF"][1] == "~" && $forceValidation !== FALSE || $forceValidation === TRUE) {
         //в домашнем каталоге включаем отладку
         $debug = TRUE;
     }
     if ($_SERVER["PHP_SELF"][1] == "~") {
         //профайлер XSLT доступен только после 5.3
         if (version_compare(PHP_VERSION, '5.3.0', '>')) {
             //разводим файлы по разным хостам - для отладки достаточно
             $xsltProfiler = "/tmp/XML_Output_profiling_" . $_SERVER["REMOTE_ADDR"] . "_" . (isset($_SERVER["USER"]) ? $_SERVER["USER"] : (isset($_SERVER["UID"]) ? $_SERVER["UID"] : posix_getuid())) . ".txt";
         }
     }
     //$debug=FALSE;
     $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->load($matches[1]);
                 $proc = new \XSLTProcessor();
                 if ($xsltProfiler) {
                     $proc->setProfiling($xsltProfiler);
                 }
                 $proc->importStyleSheet($xsl);
                 //регистрируем на себя обращения к файлам
                 stream_wrapper_unregister("file") or die(__FILE__ . __LINE__);
                 stream_wrapper_register("file", "\\XML_Output") or die(__FILE__ . __LINE__);
                 //вешаем на обработчик выхода ловушку - если вложенный скрипт попытается сделать exit или die
                 register_shutdown_function(array("\\XML_Output", "checkDone"));
                 //на время трансформации ставим свой специальный обработчик ошибок
                 set_error_handler(array("\\XML_Output", "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 ($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));
         if (isset($_SERVER["UNIQUE_ID"])) {
             header("Etag: XML_Output_" . $_SERVER["UNIQUE_ID"]);
         }
         echo $xsltResult;
     } else {
         unset($xsltResult);
         header("Content-type: application/xml");
         //header("Content-Length: ".mb_strlen($data,"ASCII"));
         echo $data;
     }
 }