Completed
Push — develop ( 8eb671...133594 )
by Mike
19:30 queued 09:24
created

src/phpDocumentor/Transformer/Writer/Xsl.php (1 issue)

Check for loose comparison of strings.

Best Practice Bug Major

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\Transformer\Writer\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) {
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