Completed
Push — develop ( 80740b...61b5c3 )
by Mike
10:20
created

Xsl::writeToFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 3
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * This file is part of phpDocumentor.
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @author    Mike van Riel <[email protected]>
11
 * @copyright 2010-2018 Mike van Riel / Naenius (http://www.naenius.com)
12
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
13
 * @link      http://phpdoc.org
14
 */
15
16
namespace phpDocumentor\Transformer\Writer;
17
18
use phpDocumentor\Application;
19
use phpDocumentor\Descriptor\ProjectDescriptor;
20
use phpDocumentor\Event\Dispatcher;
21
use phpDocumentor\Plugin\Core\Xslt\Extension;
22
use phpDocumentor\Transformer\Event\PreXslWriterEvent;
23
use phpDocumentor\Transformer\Exception;
24
use phpDocumentor\Transformer\Router\ForFileProxy;
25
use phpDocumentor\Transformer\Router\Queue;
26
use phpDocumentor\Transformer\Transformation;
27
use phpDocumentor\Transformer\Transformation as TransformationObject;
28
use phpDocumentor\Transformer\Writer\Exception\RequirementMissing;
29
use Psr\Log\LoggerInterface;
30
use XSLTProcessor;
31
32
/**
33
 * XSL transformation writer; generates static HTML out of the structure and XSL templates.
34
 */
35
class Xsl extends WriterAbstract implements Routable
36
{
37
    /** @var LoggerInterface $logger */
38
    protected $logger;
39
40
    /** @var string[] */
41
    protected $xsl_variables = [];
42
43
    /** @var Queue */
44
    private $routers;
45
46
    /**
47
     * Initialize this writer with the logger so that it can output logs.
48
     */
49
    public function __construct(LoggerInterface $logger)
50
    {
51
        $this->logger = $logger;
52
    }
53
54
    /**
55
     * Checks whether XSL handling is enabled with PHP as that is not enabled by default.
56
     *
57
     * To enable XSL handling you need either the xsl extension or the xslcache extension installed.
58
     *
59
     * @throws RequirementMissing if neither xsl extensions are installed.
60
     */
61
    public function checkRequirements()
62
    {
63
        if (!class_exists('XSLTProcessor') && (!extension_loaded('xslcache'))) {
64
            throw new RequirementMissing(
65
                'The XSL writer was unable to find your XSLTProcessor; '
66
                . 'please check if you have installed the PHP XSL extension or XSLCache extension'
67
            );
68
        }
69
    }
70
71
    /**
72
     * Sets the routers that can be used to determine the path of links.
73
     */
74
    public function setRouters(Queue $routers)
75
    {
76
        $this->routers = $routers;
77
    }
78
79
    /**
80
     * This method combines the structure.xml and the given target template
81
     * and creates a static html page at the artifact location.
82
     *
83
     * @param ProjectDescriptor $project        Document containing the structure.
84
     * @param Transformation    $transformation Transformation to execute.
85
     *
86
     * @throws \RuntimeException if the structure.xml file could not be found.
87
     * @throws Exception if the structure.xml file's documentRoot could not be read because of encoding issues
88
     *    or because it was absent.
89
     */
90
    public function transform(ProjectDescriptor $project, Transformation $transformation)
91
    {
92
        Extension::$routers = $this->routers;
93
        Extension::$projectDescriptor = $project;
94
95
        $structure = $this->loadAst($this->getAstPath($transformation));
96
97
        $proc = $this->getXslProcessor($transformation);
98
        $proc->registerPHPFunctions();
99
        $this->registerDefaultVariables($transformation, $proc, $structure);
100
        $this->setProcessorParameters($transformation, $proc);
101
102
        $artifact = $this->getArtifactPath($transformation);
103
104
        $this->checkForSpacesInPath($artifact);
105
106
        // if a query is given, then apply a transformation to the artifact
107
        // location by replacing ($<var>} with the sluggified node-value of the
108
        // search result
109
        if ($transformation->getQuery() !== '') {
110
            $xpath = new \DOMXPath($structure);
111
112
            /** @var \DOMNodeList $qry */
113
            $qry = $xpath->query($transformation->getQuery());
114
            $count = $qry->length;
115
            foreach ($qry as $key => $element) {
116
                /** @var PreXslWriterEvent $event */
117
                $event = PreXslWriterEvent::createInstance($this);
118
                $event->setElement($element);
119
                $event->setProgress([$key + 1, $count]);
120
                Dispatcher::getInstance()->dispatch(
121
                    'transformer.writer.xsl.pre',
122
                    $event
123
                );
124
125
                $proc->setParameter('', $element->nodeName, $element->nodeValue);
126
                $file_name = $transformation->getTransformer()->generateFilename(
127
                    $element->nodeValue
128
                );
129
130
                if (! $artifact) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $artifact of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
131
                    $url = $this->generateUrlForXmlElement($project, $element);
132
                    if ($url === false || $url[0] !== DIRECTORY_SEPARATOR) {
133
                        continue;
134
                    }
135
136
                    $filename = $transformation->getTransformer()->getTarget()
137
                        . str_replace('/', DIRECTORY_SEPARATOR, $url);
138
                } else {
139
                    $filename = str_replace('{$' . $element->nodeName . '}', $file_name, $artifact);
140
                }
141
142
                $relativeFileName = substr($filename, strlen($transformation->getTransformer()->getTarget()) + 1);
143
                $proc->setParameter('', 'root', str_repeat('../', substr_count($relativeFileName, '/')));
144
145
                $this->writeToFile($filename, $proc, $structure);
146
            }
147
        } else {
148
            if (substr($transformation->getArtifact(), 0, 1) === '$') {
149
                // not a file, it must become a variable!
150
                $variable_name = substr($transformation->getArtifact(), 1);
151
                $this->xsl_variables[$variable_name] = $proc->transformToXml($structure);
152
            } else {
153
                $relativeFileName = substr($artifact, strlen($transformation->getTransformer()->getTarget()) + 1);
154
                $proc->setParameter('', 'root', str_repeat('../', substr_count($relativeFileName, '/')));
155
156
                $this->writeToFile($artifact, $proc, $structure);
157
            }
158
        }
159
    }
160
161
    /**
162
     * Takes the filename and converts it into a correct URI for XSLTProcessor.
163
     *
164
     * @param string $filename
165
     *
166
     * @return string
167
     */
168
    protected function getXsltUriFromFilename($filename)
169
    {
170
        // Windows requires an additional / after the scheme. If not provided then errno 22 (I/O Error: Invalid
171
        // Argument) will be raised. Thanks to @FnTmLV for finding the cause. See issue #284 for more information.
172
        // An exception to the above is when running from a Phar file; in this case the stream is handled as if on
173
        // linux; see issue #713 for more information on this exception.
174
        if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && ! \Phar::running()) {
175
            $filename = '/' . $filename;
176
        }
177
178
        return 'file://' . $filename;
179
    }
180
181
    /**
182
     * Sets the parameters of the XSLT processor.
183
     *
184
     * @param XSLTProcessor $proc XSLTProcessor.
185
     */
186
    public function setProcessorParameters(TransformationObject $transformation, $proc)
187
    {
188
        foreach ($this->xsl_variables as $key => $variable) {
189
            // XSL does not allow both single and double quotes in a string
190
            if ((strpos($variable, '"') !== false)
191
                && ((strpos($variable, "'") !== false))
192
            ) {
193
                $this->logger->warning(
194
                    'XSLT does not allow both double and single quotes in '
195
                    . 'a variable; transforming single quotes to a character '
196
                    . 'encoded version in variable: ' . $key
197
                );
198
                $variable = str_replace("'", '&#39;', $variable);
199
            }
200
201
            $proc->setParameter('', $key, $variable);
202
        }
203
204
        // add / overwrite the parameters with those defined in the
205
        // transformation entry
206
        $parameters = $transformation->getParameters();
207
        if (isset($parameters['variables'])) {
208
            /** @var \DOMElement $variable */
209
            foreach ($parameters['variables'] as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $parameters['variables'] of type object<phpDocumentor\Tra...mer\Template\Parameter> is not traversable.
Loading history...
210
                $proc->setParameter('', $key, $value);
211
            }
212
        }
213
    }
214
215
    /**
216
     * @throws Exception
217
     */
218
    protected function getXslProcessor(Transformation $transformation): \XSLTProcessor
219
    {
220
        $xslTemplatePath = $transformation->getSourceAsPath();
221
        $this->logger->debug('Loading XSL template: ' . $xslTemplatePath);
222
        if (!file_exists($xslTemplatePath)) {
223
            throw new Exception('Unable to find XSL template "' . $xslTemplatePath . '"');
224
        }
225
226
        $xsl = new \DOMDocument();
227
        $xsl->load($xslTemplatePath);
228
229
        $proc = new \XSLTProcessor();
230
        $proc->importStylesheet($xsl);
231
232
        return $proc;
233
    }
234
235
    /**
236
     * @param string $structureFilename
237
     */
238
    private function loadAst($structureFilename): \DOMDocument
239
    {
240
        if (!is_readable($structureFilename)) {
241
            throw new \RuntimeException(
242
                'Structure.xml file was not found in the target directory, is the XML writer missing from the '
243
                . 'template definition?'
244
            );
245
        }
246
247
        $structure = new \DOMDocument('1.0', 'utf-8');
248
        libxml_use_internal_errors(true);
249
        $structure->load($structureFilename);
250
251
        if (empty($structure->documentElement)) {
252
            $message = 'Specified DOMDocument lacks documentElement, cannot transform.';
253
            $error = libxml_get_last_error();
254
            if ($error) {
255
                $message .= PHP_EOL . 'Apparently an error occurred with reading the structure.xml file, the reported '
256
                    . 'error was "' . trim($error->message) . '" on line ' . $error->line;
257
            }
258
259
            throw new Exception($message);
260
        }
261
262
        return $structure;
263
    }
264
265
    private function registerDefaultVariables(
266
        Transformation $transformation,
267
        \XSLTProcessor $proc,
268
        \DOMDocument $structure
269
    ) {
270
        $proc->setParameter('', 'title', $structure->documentElement->getAttribute('title'));
271
272
        if ($transformation->getParameter('search') !== null && $transformation->getParameter('search')->getValue()) {
273
            $proc->setParameter('', 'search_template', $transformation->getParameter('search')->getValue());
274
        } else {
275
            $proc->setParameter('', 'search_template', 'none');
276
        }
277
278
        $proc->setParameter('', 'version', Application::VERSION());
279
        $proc->setParameter('', 'generated_datetime', date('r'));
280
    }
281
282
    /**
283
     * @param string $filename
284
     */
285
    private function writeToFile($filename, \XSLTProcessor $proc, \DOMDocument $structure)
286
    {
287
        if (!file_exists(dirname($filename))) {
288
            mkdir(dirname($filename), 0755, true);
289
        }
290
291
        $proc->transformToUri($structure, $this->getXsltUriFromFilename($filename));
292
    }
293
294
    private function getAstPath(Transformation $transformation)
295
    {
296
        return $transformation->getTransformer()->getTarget() . DIRECTORY_SEPARATOR . 'structure.xml';
297
    }
298
299
    /**
300
     * Returns the path to the location where the artifact should be written, or null to automatically detect the
301
     * location using the router.
302
     *
303
     * @return string|null
304
     */
305
    private function getArtifactPath(Transformation $transformation)
306
    {
307
        return $transformation->getArtifact()
308
            ? $transformation->getTransformer()->getTarget() . DIRECTORY_SEPARATOR . $transformation->getArtifact()
309
            : null;
310
    }
311
312
    /**
313
     * @return bool|string
314
     */
315
    private function generateUrlForXmlElement(ProjectDescriptor $project, \DOMElement $element)
316
    {
317
        $elements = $project->getIndexes()->get('elements');
318
319
        $elementFqcn = ($element->parentNode->nodeName === 'namespace' ? '~\\' : '') . $element->nodeValue;
320
        $node = $elements[$elementFqcn]
321
            ?? $element->nodeValue; // do not use the normalized version if the element is not found!
322
323
        $rule = $this->routers->match($node);
324
        if (!$rule) {
325
            throw new \InvalidArgumentException(
326
                'No matching routing rule could be found for the given node, please provide an artifact location, '
327
                . 'encountered: ' . ($node === null ? 'NULL' : get_class($node))
328
            );
329
        }
330
331
        $rule = new ForFileProxy($rule);
332
        return $rule->generate($node);
333
    }
334
}
335