1
|
|
|
<?php |
2
|
|
|
namespace Agavi\Config; |
3
|
|
|
|
4
|
|
|
// +---------------------------------------------------------------------------+ |
5
|
|
|
// | This file is part of the Agavi package. | |
6
|
|
|
// | Copyright (c) 2005-2011 the Agavi Project. | |
7
|
|
|
// | | |
8
|
|
|
// | For the full copyright and license information, please view the LICENSE | |
9
|
|
|
// | file that was distributed with this source code. You can also view the | |
10
|
|
|
// | LICENSE file online at http://www.agavi.org/LICENSE.txt | |
11
|
|
|
// | vi: set noexpandtab: | |
12
|
|
|
// | Local Variables: | |
13
|
|
|
// | indent-tabs-mode: t | |
14
|
|
|
// | End: | |
15
|
|
|
// +---------------------------------------------------------------------------+ |
16
|
|
|
|
17
|
|
|
|
18
|
|
|
use Agavi\Util\SchematronProcessor; |
19
|
|
|
use Agavi\Util\Toolkit; |
20
|
|
|
use Agavi\Exception\ParseException; |
21
|
|
|
use Agavi\Exception\UnreadableException; |
22
|
|
|
use Agavi\Config\Util\Dom\XmlConfigDomDocument; |
23
|
|
|
use Agavi\Config\Util\Dom\XmlConfigDomElement; |
24
|
|
|
use Agavi\Util\XsltProcessor; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* XmlConfigParser handles both Agavi and foreign XML configuration files, |
28
|
|
|
* deals with XIncludes, XSL transformations and validation as well as filtering |
29
|
|
|
* and ordering of configuration blocks and parent file resolution and parsing. |
30
|
|
|
* |
31
|
|
|
* @package agavi |
32
|
|
|
* @subpackage config |
33
|
|
|
* |
34
|
|
|
* @author David Zülke <[email protected]> |
35
|
|
|
* @author Noah Fontes <[email protected]> |
36
|
|
|
* @copyright Authors |
37
|
|
|
* @copyright The Agavi Project |
38
|
|
|
* |
39
|
|
|
* @since 0.11.0 |
40
|
|
|
* |
41
|
|
|
* @version $Id$ |
42
|
|
|
*/ |
43
|
|
|
|
44
|
|
|
class XmlConfigParser |
45
|
|
|
{ |
46
|
|
|
const NAMESPACE_AGAVI_ENVELOPE_0_11 = 'http://agavi.org/agavi/1.0/config'; |
47
|
|
|
|
48
|
|
|
const NAMESPACE_AGAVI_ENVELOPE_1_0 = 'http://agavi.org/agavi/config/global/envelope/1.0'; |
49
|
|
|
|
50
|
|
|
const NAMESPACE_AGAVI_ENVELOPE_1_1 = 'http://agavi.org/agavi/config/global/envelope/1.1'; |
51
|
|
|
|
52
|
|
|
const NAMESPACE_AGAVI_ENVELOPE_LATEST = self::NAMESPACE_AGAVI_ENVELOPE_1_1; |
53
|
|
|
|
54
|
|
|
const NAMESPACE_AGAVI_ANNOTATIONS_1_0 = 'http://agavi.org/agavi/config/global/annotations/1.0'; |
55
|
|
|
|
56
|
|
|
const NAMESPACE_AGAVI_ANNOTATIONS_LATEST = self::NAMESPACE_AGAVI_ANNOTATIONS_1_0; |
57
|
|
|
|
58
|
|
|
const VALIDATION_TYPE_XMLSCHEMA = 'xml_schema'; |
59
|
|
|
|
60
|
|
|
const VALIDATION_TYPE_RELAXNG = 'relax_ng'; |
61
|
|
|
|
62
|
|
|
const VALIDATION_TYPE_SCHEMATRON = 'schematron'; |
63
|
|
|
|
64
|
|
|
const NAMESPACE_SCHEMATRON_ISO = 'http://purl.oclc.org/dsdl/schematron'; |
65
|
|
|
|
66
|
|
|
const NAMESPACE_SVRL_ISO = 'http://purl.oclc.org/dsdl/svrl'; |
67
|
|
|
|
68
|
|
|
const NAMESPACE_XML_1998 = 'http://www.w3.org/XML/1998/namespace'; |
69
|
|
|
|
70
|
|
|
const NAMESPACE_XMLNS_2000 = 'http://www.w3.org/2000/xmlns/'; |
71
|
|
|
|
72
|
|
|
const NAMESPACE_XSL_1999 = 'http://www.w3.org/1999/XSL/Transform'; |
73
|
|
|
|
74
|
|
|
const NAMESPACE_XINCLUDE_2001 = 'http://www.w3.org/2001/XInclude'; |
75
|
|
|
|
76
|
|
|
const STAGE_SINGLE = 'single'; |
77
|
|
|
|
78
|
|
|
const STAGE_COMPILATION = 'compilation'; |
79
|
|
|
|
80
|
|
|
const STEP_TRANSFORMATIONS_BEFORE = 'transformations_before'; |
81
|
|
|
|
82
|
|
|
const STEP_TRANSFORMATIONS_AFTER = 'transformations_after'; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* @var array A list of XML namespaces for Agavi configuration files as |
86
|
|
|
* keys and their associated XPath namespace prefix (value). |
87
|
|
|
*/ |
88
|
|
|
public static $agaviEnvelopeNamespaces = array( |
89
|
|
|
self::NAMESPACE_AGAVI_ENVELOPE_0_11 => 'agavi_envelope_0_11', |
90
|
|
|
self::NAMESPACE_AGAVI_ENVELOPE_1_0 => 'agavi_envelope_1_0', |
91
|
|
|
self::NAMESPACE_AGAVI_ENVELOPE_1_1 => 'agavi_envelope_1_1', |
92
|
|
|
); |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* @var array A list of all XML namespaces that are used internally by |
96
|
|
|
* the configuration parser. |
97
|
|
|
*/ |
98
|
|
|
public static $agaviNamespaces = array( |
99
|
|
|
self::NAMESPACE_AGAVI_ENVELOPE_0_11 => 'agavi_envelope_0_11', |
100
|
|
|
self::NAMESPACE_AGAVI_ENVELOPE_1_0 => 'agavi_envelope_1_0', |
101
|
|
|
self::NAMESPACE_AGAVI_ENVELOPE_1_1 => 'agavi_envelope_1_1', |
102
|
|
|
self::NAMESPACE_AGAVI_ANNOTATIONS_1_0 => 'agavi_annotations_1_0', |
103
|
|
|
); |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* @var string Path to the config file we're parsing in this instance. |
107
|
|
|
*/ |
108
|
|
|
protected $path = ''; |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* @var string The name of the current environment. |
112
|
|
|
*/ |
113
|
|
|
protected $environment = ''; |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* @var string The name of the current context. |
117
|
|
|
*/ |
118
|
|
|
protected $context = null; |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* @var \DOMDocument|XmlConfigDomDocument The document we're parsing here. |
122
|
|
|
*/ |
123
|
|
|
protected $doc = null; |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Test if the given document looks like an Agavi config file. |
127
|
|
|
* |
128
|
|
|
* @param \DOMDocument $doc The document to test. |
129
|
|
|
* |
130
|
|
|
* @return bool True, if it is an Agavi config document, false otherwise. |
131
|
|
|
* |
132
|
|
|
* @author David Zülke <[email protected]> |
133
|
|
|
* @since 1.0.0 |
134
|
|
|
*/ |
135
|
|
|
public static function isAgaviConfigurationDocument(\DOMDocument $doc) |
136
|
|
|
{ |
137
|
|
|
return $doc->documentElement && $doc->documentElement->localName == 'configurations' && self::isAgaviEnvelopeNamespace($doc->documentElement->namespaceURI); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Check if the given namespace URI is a valid Agavi envelope namespace. |
142
|
|
|
* |
143
|
|
|
* @param string $namespaceUri The namespace URI. |
144
|
|
|
* |
145
|
|
|
* @return bool True, if the given URI is a valid namespace URI, or false. |
146
|
|
|
* |
147
|
|
|
* @author David Zülke <[email protected]> |
148
|
|
|
* @since 1.0.0 |
149
|
|
|
*/ |
150
|
|
|
public static function isAgaviEnvelopeNamespace($namespaceUri) |
151
|
|
|
{ |
152
|
|
|
return isset(self::$agaviEnvelopeNamespaces[$namespaceUri]); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Check if a given namespace URI is a valid Agavi namespace. |
157
|
|
|
* |
158
|
|
|
* @param string $namespaceUri The namespace URI. |
159
|
|
|
* |
160
|
|
|
* @return bool True if the given URI is a valid namespace URI, |
161
|
|
|
* false otherwise. |
162
|
|
|
* |
163
|
|
|
* @author Noah Fontes <[email protected]> |
164
|
|
|
* @since 1.0.0 |
165
|
|
|
*/ |
166
|
|
|
public static function isAgaviNamespace($namespaceUri) |
167
|
|
|
{ |
168
|
|
|
return isset(self::$agaviNamespaces[$namespaceUri]); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* Retrieves an XPath namespace prefix based on a given namespace URI. |
173
|
|
|
* |
174
|
|
|
* @param string $namespaceUri The namespace URI. |
175
|
|
|
* |
176
|
|
|
* @return string The prefix for the namespace URI, or null if none |
177
|
|
|
* exists. |
178
|
|
|
* |
179
|
|
|
* @author Noah Fontes <[email protected]> |
180
|
|
|
* @since 1.0.0 |
181
|
|
|
*/ |
182
|
|
|
public static function getAgaviNamespacePrefix($namespaceUri) |
183
|
|
|
{ |
184
|
|
|
if (self::isAgaviNamespace($namespaceUri)) { |
185
|
|
|
return self::$agaviNamespaces[$namespaceUri]; |
186
|
|
|
} |
187
|
|
|
return null; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* Register Agavi namespace prefixes in a given document. |
192
|
|
|
* |
193
|
|
|
* @param XmlConfigDomDocument $document The document. |
194
|
|
|
* |
195
|
|
|
* @author Noah Fontes <[email protected]> |
196
|
|
|
* @since 1.0.0 |
197
|
|
|
*/ |
198
|
|
|
public static function registerAgaviNamespaces(XmlConfigDomDocument $document) |
199
|
|
|
{ |
200
|
|
|
$xpath = $document->getXpath(); |
201
|
|
|
|
202
|
|
|
foreach (self::$agaviNamespaces as $namespaceUri => $prefix) { |
203
|
|
|
$xpath->registerNamespace($prefix, $namespaceUri); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/* Register the latest namespaces. */ |
207
|
|
|
$xpath->registerNamespace('agavi_envelope_latest', self::NAMESPACE_AGAVI_ENVELOPE_LATEST); |
208
|
|
|
$xpath->registerNamespace('agavi_annotations_latest', self::NAMESPACE_AGAVI_ANNOTATIONS_LATEST); |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* @param string $path An absolute filesystem path to a configuration file. |
213
|
|
|
* @param string $environment The environment name. |
214
|
|
|
* @param string $context The optional context name. |
215
|
|
|
* @param array $transformationInfo An associative array of transformation information. |
216
|
|
|
* @param array $validationInfo An associative array of validation information. |
217
|
|
|
* |
218
|
|
|
* @return XmlConfigDomDocument A properly merged DOMDocument. |
219
|
|
|
* |
220
|
|
|
* @author David Zülke <[email protected]> |
221
|
|
|
* @author Dominik del Bondio <[email protected]> |
222
|
|
|
* @author Noah Fontes <[email protected]> |
223
|
|
|
* @since 0.11.0 |
224
|
|
|
*/ |
225
|
|
|
public static function run($path, $environment, $context = null, array $transformationInfo = array(), array $validationInfo = array()) |
226
|
|
|
{ |
227
|
|
|
$isAgaviConfigFormat = true; |
228
|
|
|
// build an array of documents (this one, and the parents) |
229
|
|
|
$docs = array(); |
230
|
|
|
$previousPaths = array(); |
231
|
|
|
$nextPath = $path; |
232
|
|
|
while ($nextPath !== null) { |
233
|
|
|
// run the single stage parser |
234
|
|
|
$parser = new XmlConfigParser($nextPath, $environment, $context); |
235
|
|
|
$doc = $parser->execute($transformationInfo[self::STAGE_SINGLE], $validationInfo[self::STAGE_SINGLE]); |
236
|
|
|
|
237
|
|
|
// put the new document in the list |
238
|
|
|
$docs[] = $doc; |
239
|
|
|
|
240
|
|
|
// make sure it (still) is a <configurations> file with the proper Agavi namespace |
241
|
|
|
if ($isAgaviConfigFormat) { |
242
|
|
|
$isAgaviConfigFormat = self::isAgaviConfigurationDocument($doc); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
// is it an Agavi <configurations> element? does it have a parent attribute? yes? good. parse that next |
246
|
|
|
// TODO: support future namespaces |
247
|
|
|
if ($isAgaviConfigFormat && $doc->documentElement->hasAttribute('parent')) { |
248
|
|
|
$theNextPath = Toolkit::literalize($doc->documentElement->getAttribute('parent')); |
249
|
|
|
|
250
|
|
|
// no infinite loop plz, kthx |
251
|
|
|
if ($nextPath === $theNextPath) { |
252
|
|
|
throw new ParseException(sprintf("Agavi detected an infinite loop while processing parent configuration files of \n%s\n\nFile\n%s\nincludes itself as a parent.", $path, $theNextPath)); |
253
|
|
|
} elseif (isset($previousPaths[$theNextPath])) { |
254
|
|
|
throw new ParseException(sprintf("Agavi detected an infinite loop while processing parent configuration files of \n%s\n\nFile\n%s\nhas previously been included by\n%s", $path, $theNextPath, $previousPaths[$theNextPath])); |
255
|
|
|
} else { |
256
|
|
|
$previousPaths[$theNextPath] = $nextPath; |
257
|
|
|
$nextPath = $theNextPath; |
258
|
|
|
} |
259
|
|
|
} else { |
260
|
|
|
$nextPath = null; |
261
|
|
|
} |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
// TODO: use our own classes here that extend DOM* |
265
|
|
|
$retval = new XmlConfigDomDocument(); |
266
|
|
|
foreach (self::$agaviEnvelopeNamespaces as $envelopeNamespaceUri => $envelopeNamespacePrefix) { |
267
|
|
|
$retval->getXpath()->registerNamespace($envelopeNamespacePrefix, $envelopeNamespaceUri); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
if ($isAgaviConfigFormat) { |
271
|
|
|
// if it is an Agavi config, we'll create a new document with all files' <configuration> blocks inside |
272
|
|
|
$retval->appendChild(new XmlConfigDomElement('configurations', null, self::NAMESPACE_AGAVI_ENVELOPE_LATEST)); |
273
|
|
|
|
274
|
|
|
// reverse the array - we want the parents first! |
275
|
|
|
$docs = array_reverse($docs); |
276
|
|
|
|
277
|
|
|
$configurationElements = array(); |
278
|
|
|
|
279
|
|
|
// TODO: I bet this leaks memory due to the nodes being taken out of the docs. beware circular refs! |
280
|
|
|
/** @var XmlConfigDomDocument $doc */ |
281
|
|
|
foreach ($docs as $doc) { |
282
|
|
|
// iterate over all nodes (attributes, <sandbox>, <configuration> etc) inside the document element and append them to the <configurations> element in our final document |
283
|
|
|
foreach ($doc->documentElement->childNodes as $node) { |
284
|
|
|
if ($node->nodeType == XML_ELEMENT_NODE && $node->localName == 'configuration' && self::isAgaviEnvelopeNamespace($node->namespaceURI)) { |
285
|
|
|
// it's a <configuration> element - put that on a stack for processing |
286
|
|
|
$configurationElements[] = $node; |
287
|
|
|
} else { |
288
|
|
|
// import the node, recursively, and store the imported node |
289
|
|
|
$importedNode = $retval->importNode($node, true); |
290
|
|
|
// now append it to the <configurations> element |
291
|
|
|
$retval->documentElement->appendChild($importedNode); |
292
|
|
|
} |
293
|
|
|
} |
294
|
|
|
// if it's a <configurations> element, then we need to copy the attributes from there |
295
|
|
|
if ($doc->isAgaviConfiguration()) { |
296
|
|
|
$namespaces = $doc->getXPath()->query('namespace::*'); |
297
|
|
|
foreach ($namespaces as $namespace) { |
298
|
|
|
if ($namespace->localName !== 'xml' && $namespace->localName != 'xmlns') { |
299
|
|
|
$retval->documentElement->setAttributeNS(self::NAMESPACE_XMLNS_2000, 'xmlns:' . $namespace->localName, $namespace->namespaceURI); |
300
|
|
|
} |
301
|
|
|
} |
302
|
|
|
foreach ($doc->documentElement->attributes as $attribute) { |
303
|
|
|
// but not the "parent" attributes... |
304
|
|
|
if ($attribute->namespaceURI === null && $attribute->localName === 'parent') { |
305
|
|
|
continue; |
306
|
|
|
} |
307
|
|
|
$importedAttribute = $retval->importNode($attribute, true); |
308
|
|
|
$retval->documentElement->setAttributeNode($importedAttribute); |
309
|
|
|
} |
310
|
|
|
} |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
// generic <configuration> first, then those with an environment attribute, then those with context, then those with both |
314
|
|
|
$configurationOrder = array( |
315
|
|
|
'count(self::node()[@agavi_annotations_latest:matched and not(@environment) and not(@context)])', |
316
|
|
|
'count(self::node()[@agavi_annotations_latest:matched and @environment and not(@context)])', |
317
|
|
|
'count(self::node()[@agavi_annotations_latest:matched and not(@environment) and @context])', |
318
|
|
|
'count(self::node()[@agavi_annotations_latest:matched and @environment and @context])', |
319
|
|
|
); |
320
|
|
|
|
321
|
|
|
// now we sort the nodes according to the rules |
322
|
|
|
foreach ($configurationOrder as $xpath) { |
323
|
|
|
// append all matching nodes from the order array... |
324
|
|
|
foreach ($configurationElements as &$element) { |
325
|
|
|
// ... if the xpath matches, that is! |
326
|
|
|
if ($element->ownerDocument->getXpath()->evaluate($xpath, $element)) { |
327
|
|
|
// it did, so import the node and append it to the result doc |
328
|
|
|
$importedNode = $retval->importNode($element, true); |
329
|
|
|
$retval->documentElement->appendChild($importedNode); |
330
|
|
|
} |
331
|
|
|
} |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
// run the compilation stage parser |
335
|
|
|
$retval = self::executeCompilation($retval, $environment, $context, $transformationInfo[self::STAGE_COMPILATION], $validationInfo[self::STAGE_COMPILATION]); |
336
|
|
|
} else { |
337
|
|
|
// it's not an agavi config file. just pass it through then |
338
|
|
|
$retval->appendChild($retval->importNode($doc->documentElement, true)); |
|
|
|
|
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
// cleanup attempt |
342
|
|
|
unset($docs); |
343
|
|
|
|
344
|
|
|
// set the pseudo-document URI |
345
|
|
|
$retval->documentURI = $path; |
346
|
|
|
|
347
|
|
|
return $retval; |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* Builds a proper regular expression from the input pattern to test against |
352
|
|
|
* the given subject. This is for "environment" and "context" attributes of |
353
|
|
|
* configuration blocks in the files. |
354
|
|
|
* |
355
|
|
|
* @param string $pattern A regular expression chunk without delimiters/anchors. |
356
|
|
|
* @param string $subject The subject to test against |
357
|
|
|
* |
358
|
|
|
* @return bool Whether or not the subject matched the pattern. |
359
|
|
|
* |
360
|
|
|
* @author David Zülke <[email protected]> |
361
|
|
|
* @since 1.0.0 |
362
|
|
|
*/ |
363
|
|
|
public static function testPattern($pattern, $subject) |
364
|
|
|
{ |
365
|
|
|
// four backslashes mean one literal backslash |
366
|
|
|
$pattern = preg_replace('/\\\\+#/', '\\#', $pattern); |
367
|
|
|
return (preg_match('#^(' . implode('|', array_map('trim', explode(' ', $pattern))) . ')$#', $subject) > 0); |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* The constructor. |
372
|
|
|
* Will make a DOMDocument instance using the given path. |
373
|
|
|
* |
374
|
|
|
* @param string $path The path to the configuration file. |
375
|
|
|
* @param string $environment The optional name of the current environment. |
376
|
|
|
* @param string $context The optional name of the current context. |
377
|
|
|
* |
378
|
|
|
* @author David Zülke <[email protected]> |
379
|
|
|
* @author Noah Fontes <[email protected]> |
380
|
|
|
* @since 1.0.0 |
381
|
|
|
*/ |
382
|
|
|
public function __construct($path, $environment = null, $context = null) |
383
|
|
|
{ |
384
|
|
|
// store environment... |
385
|
|
|
if ($environment === null) { |
386
|
|
|
$environment = Config::get('core.environment'); |
387
|
|
|
} |
388
|
|
|
$this->environment = $environment; |
389
|
|
|
// ... and context names |
390
|
|
|
$this->context = $context; |
391
|
|
|
|
392
|
|
View Code Duplication |
if (!is_readable($path)) { |
|
|
|
|
393
|
|
|
$error = 'Configuration file "' . $path . '" does not exist or is unreadable'; |
394
|
|
|
throw new UnreadableException($error); |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
// store path to the config file |
398
|
|
|
$this->path = $path; |
399
|
|
|
|
400
|
|
|
// AgaviXmlConfigDomDocument has convenience methods! |
401
|
|
|
try { |
402
|
|
|
$this->doc = new XmlConfigDomDocument(); |
403
|
|
|
$this->doc->substituteEntities = true; |
404
|
|
|
$this->doc->load($path); |
405
|
|
|
} catch (\DOMException $dome) { |
406
|
|
|
throw new ParseException(sprintf('Configuration file "%s" could not be parsed: %s', $path, $dome->getMessage()), 0, $dome); |
407
|
|
|
} |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
/** |
411
|
|
|
* Destructor to do the cleaning up. |
412
|
|
|
* |
413
|
|
|
* @author David Zülke <[email protected]> |
414
|
|
|
* @since 1.0.0 |
415
|
|
|
*/ |
416
|
|
|
public function __destruct() |
417
|
|
|
{ |
418
|
|
|
unset($this->doc); |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
/** |
422
|
|
|
* @param array $transformationInfo An array of XSL paths for transformation. |
423
|
|
|
* @param array $validationInfo An associative array of validation information. |
424
|
|
|
* |
425
|
|
|
* @return \DOMDocument Our DOMDocument. |
426
|
|
|
* |
427
|
|
|
* @author David Zülke <[email protected]> |
428
|
|
|
* @author Dominik del Bondio <[email protected]> |
429
|
|
|
* @author Noah Fontes <[email protected]> |
430
|
|
|
* @since 0.11.0 |
431
|
|
|
*/ |
432
|
|
|
public function execute(array $transformationInfo = array(), array $validationInfo = array()) |
433
|
|
|
{ |
434
|
|
|
// resolve xincludes |
435
|
|
|
self::xinclude($this->doc); |
|
|
|
|
436
|
|
|
|
437
|
|
|
// validate XMLSchema-instance declarations |
438
|
|
|
self::validateXsi($this->doc); |
|
|
|
|
439
|
|
|
|
440
|
|
|
// validate pre-transformation |
441
|
|
|
self::validate($this->doc, $this->environment, $this->context, $validationInfo[XmlConfigParser::STEP_TRANSFORMATIONS_BEFORE]); |
|
|
|
|
442
|
|
|
|
443
|
|
|
// mark document for merging |
444
|
|
|
self::match($this->doc, $this->environment, $this->context); |
|
|
|
|
445
|
|
|
|
446
|
|
|
if (!Config::get('core.skip_config_transformations', false)) { |
|
|
|
|
447
|
|
|
// run inline transformations |
448
|
|
|
$this->doc = self::transformProcessingInstructions($this->doc, $this->environment, $this->context); |
|
|
|
|
449
|
|
|
|
450
|
|
|
// perform XSL transformations |
451
|
|
|
$this->doc = self::transform($this->doc, $this->environment, $this->context, $transformationInfo); |
452
|
|
|
|
453
|
|
|
// resolve xincludes again, since transformations may have introduced some |
454
|
|
|
self::xinclude($this->doc); |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
// validate post-transformation |
458
|
|
|
self::validate($this->doc, $this->environment, $this->context, $validationInfo[XmlConfigParser::STEP_TRANSFORMATIONS_AFTER]); |
|
|
|
|
459
|
|
|
|
460
|
|
|
// clean up the document |
461
|
|
|
self::cleanup($this->doc); |
|
|
|
|
462
|
|
|
|
463
|
|
|
return $this->doc; |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
/** |
467
|
|
|
* Executes the parser for a compilation document. |
468
|
|
|
* |
469
|
|
|
* @param XmlConfigDomDocument $document The document to act upon. |
470
|
|
|
* @param string $environment The environment name. |
471
|
|
|
* @param string $context The context name. |
472
|
|
|
* @param array $transformationInfo An array of XSL paths for transformation. |
473
|
|
|
* @param array $validationInfo An associative array of validation information. |
474
|
|
|
* |
475
|
|
|
* @author Noah Fontes <[email protected]> |
476
|
|
|
* @since 1.0.0 |
477
|
|
|
*/ |
478
|
|
|
public static function executeCompilation(XmlConfigDomDocument $document, $environment, $context, array $transformationInfo = array(), array $validationInfo = array()) |
479
|
|
|
{ |
480
|
|
|
// resolve xincludes |
481
|
|
|
self::xinclude($document); |
482
|
|
|
|
483
|
|
|
// validate pre-transformation |
484
|
|
|
self::validate($document, $environment, $context, $validationInfo[XmlConfigParser::STEP_TRANSFORMATIONS_BEFORE]); |
485
|
|
|
|
486
|
|
|
if (!Config::get('core.skip_config_transformations', false)) { |
|
|
|
|
487
|
|
|
// perform XSL transformations |
488
|
|
|
$document = self::transform($document, $environment, $context, $transformationInfo); |
489
|
|
|
|
490
|
|
|
// resolve xincludes again, since transformations may have introduced some |
491
|
|
|
self::xinclude($document); |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
// validate post-transformation |
495
|
|
|
self::validate($document, $environment, $context, $validationInfo[XmlConfigParser::STEP_TRANSFORMATIONS_AFTER]); |
496
|
|
|
|
497
|
|
|
return $document; |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
/** |
501
|
|
|
* Resolve xinclude directives on a given document. |
502
|
|
|
* |
503
|
|
|
* @param XmlConfigDomDocument $document The document to act upon. |
504
|
|
|
* |
505
|
|
|
* @author David Zülke <[email protected]> |
506
|
|
|
* @author Noah Fontes <[email protected]> |
507
|
|
|
* @since 1.0.0 |
508
|
|
|
*/ |
509
|
|
|
public static function xinclude(XmlConfigDomDocument $document) |
510
|
|
|
{ |
511
|
|
|
// expand directives, resolve globs and encode paths in XInclude href attributes |
512
|
|
|
$elements = $document->getElementsByTagNameNS(self::NAMESPACE_XINCLUDE_2001, 'include'); |
513
|
|
|
$length = $elements->length; |
514
|
|
|
// we can't foreach() over the DOMNodeList as we're modifying it further below |
515
|
|
|
// see http://php.net/manual/en/class.domnodelist.php#83178 |
516
|
|
|
for ($i = 0; $i < $length; $i++) { |
517
|
|
|
$element = $elements->item($i); |
518
|
|
|
if ($element->hasAttribute('href')) { |
|
|
|
|
519
|
|
|
$attribute = $element->getAttributeNode('href'); |
520
|
|
|
$parts = explode('#', $attribute->nodeValue, 2); |
521
|
|
|
$parts[0] = str_replace('\\', '/', Toolkit::expandDirectives($parts[0])); |
522
|
|
|
$attribute->nodeValue = rawurlencode($parts[0]) . (isset($parts[1]) ? '#' . $parts[1] : ''); |
523
|
|
|
if (strpos($parts[0], '*') !== false || strpos($parts[0], '{') !== false) { |
524
|
|
|
$glob = glob($parts[0], GLOB_BRACE); |
525
|
|
|
if ($glob) { |
|
|
|
|
526
|
|
|
$glob = array_unique($glob); // it could be that someone used /path/to/{Foo,*}/burp.xml so Foo would come before all others, that's why we need to remove duplicates as the * would match Foo again |
527
|
|
|
foreach ($glob as $path) { |
528
|
|
|
$new = $element->cloneNode(true); |
529
|
|
|
$new->setAttribute('href', rawurlencode($path) . (isset($parts[1]) ? '#' . $parts[1] : '')); |
530
|
|
|
$element->parentNode->insertBefore($new, $element); |
531
|
|
|
++$i; |
532
|
|
|
} |
533
|
|
|
$element->parentNode->removeChild($element); |
534
|
|
|
} |
535
|
|
|
} |
536
|
|
|
} |
537
|
|
|
} |
538
|
|
|
|
539
|
|
|
// perform xincludes |
540
|
|
|
try { |
541
|
|
|
$document->xinclude(); |
542
|
|
|
} catch (\DOMException $dome) { |
543
|
|
|
throw new ParseException(sprintf('Configuration file "%s" could not be parsed: %s', $document->documentURI, $dome->getMessage()), 0, $dome); |
544
|
|
|
} |
545
|
|
|
|
546
|
|
|
// remove all xml:base attributes inserted by XIncludes |
547
|
|
|
$nodes = $document->getXpath()->query('//@xml:base', $document); |
548
|
|
|
foreach ($nodes as $node) { |
549
|
|
|
$node->ownerElement->removeAttributeNode($node); |
550
|
|
|
} |
551
|
|
|
} |
552
|
|
|
|
553
|
|
|
/** |
554
|
|
|
* Annotate the document with matched attributes against each configuration |
555
|
|
|
* element that matches the given context and environment. |
556
|
|
|
* |
557
|
|
|
* @param XmlConfigDomDocument $document The document to act upon. |
558
|
|
|
* @param string $environment The environment name. |
559
|
|
|
* @param string $context The context name. |
560
|
|
|
* |
561
|
|
|
* @author David Zülke <[email protected]> |
562
|
|
|
* @author Noah Fontes <[email protected]> |
563
|
|
|
* @since 1.0.0 |
564
|
|
|
*/ |
565
|
|
|
public static function match(XmlConfigDomDocument $document, $environment, $context) |
566
|
|
|
{ |
567
|
|
|
if ($document->isAgaviConfiguration()) { |
568
|
|
|
// it's an agavi config, so we need to set "matched" flags on all <configuration> elements where "context" and "environment" attributes match the values below |
569
|
|
|
$testAttributes = array( |
570
|
|
|
'context' => $context, |
571
|
|
|
'environment' => $environment, |
572
|
|
|
); |
573
|
|
|
|
574
|
|
|
foreach ($document->getConfigurationElements() as $configuration) { |
575
|
|
|
// assume that the element counts as matched, in case it doesn't have "context" or "environment" attributes |
576
|
|
|
$matched = true; |
577
|
|
|
foreach ($testAttributes as $attributeName => $attributeValue) { |
578
|
|
|
if ($configuration->hasAttribute($attributeName)) { |
579
|
|
|
$matched = $matched && self::testPattern($configuration->getAttribute($attributeName), $attributeValue); |
580
|
|
|
} |
581
|
|
|
} |
582
|
|
|
if ($matched) { |
583
|
|
|
// if all was fine, we set the attribute. the element will then be kept in the merged result doc later |
584
|
|
|
$configuration->setAttributeNS(self::NAMESPACE_AGAVI_ANNOTATIONS_LATEST, 'agavi_annotations_latest:matched', 'true'); |
585
|
|
|
} |
586
|
|
|
} |
587
|
|
|
} |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
/** |
591
|
|
|
* Transform the document using info from embedded processing instructions |
592
|
|
|
* and given stylesheets. |
593
|
|
|
* |
594
|
|
|
* @param XmlConfigDomDocument $document The document to act upon. |
595
|
|
|
* @param string $environment The environment name. |
596
|
|
|
* @param string $context The context name. |
597
|
|
|
* @param array $transformationInfo An array of transformation information. |
598
|
|
|
* @param array $transformations An array of XSL stylesheets in DOMDocument instances. |
599
|
|
|
* |
600
|
|
|
* @return XmlConfigDomDocument The transformed document. |
601
|
|
|
* |
602
|
|
|
* @author David Zülke <[email protected]> |
603
|
|
|
* @author Noah Fontes <[email protected]> |
604
|
|
|
* @since 0.11.0 |
605
|
|
|
*/ |
606
|
|
|
public static function transform(XmlConfigDomDocument $document, $environment, $context, array $transformationInfo = array(), $transformations = array()) |
607
|
|
|
{ |
608
|
|
|
// loop over all the paths we found and load the files |
609
|
|
|
foreach ($transformationInfo as $href) { |
610
|
|
|
try { |
611
|
|
|
$xsl = new XmlConfigDomDocument(); |
612
|
|
|
$xsl->load($href); |
613
|
|
|
} catch (\DOMException $dome) { |
614
|
|
|
throw new ParseException(sprintf('Configuration file "%s" could not be parsed: Could not load XSL stylesheet "%s": %s', $document->documentURI, $href, $dome->getMessage()), 0, $dome); |
615
|
|
|
} |
616
|
|
|
|
617
|
|
|
// add them to the list of transformations to be done |
618
|
|
|
$transformations[] = $xsl; |
619
|
|
|
} |
620
|
|
|
|
621
|
|
|
// now let's perform the transformations |
622
|
|
|
foreach ($transformations as $xsl) { |
623
|
|
|
// load the stylesheet document into an XSLTProcessor instance |
624
|
|
|
try { |
625
|
|
|
$proc = new XsltProcessor(); |
626
|
|
|
$proc->registerPHPFunctions(); |
627
|
|
|
$proc->importStylesheet($xsl); |
628
|
|
|
} catch (\Exception $e) { |
629
|
|
|
throw new ParseException(sprintf('Configuration file "%s" could not be parsed: Could not import XSL stylesheet "%s": %s', $document->documentURI, $xsl->documentURI, $e->getMessage()), 0, $e); |
630
|
|
|
} |
631
|
|
|
|
632
|
|
|
// set some info (config file path, context name, environment name) as params |
633
|
|
|
// first arg is the namespace URI, which PHP doesn't support. awesome. see http://bugs.php.net/bug.php?id=30622 for the sad details |
634
|
|
|
// we could use "agavi:context" etc, that does work even without such a prefix being declared in the stylesheet, but that would be completely non-XML-ish, confusing, and against the spec. so we use dots instead. |
635
|
|
|
// the string casts are required for hhvm ($context could be null for example and hhvm bails out on that) |
636
|
|
|
$proc->setParameter('', array( |
637
|
|
|
'agavi.config_path' => (string)$document->documentURI, |
638
|
|
|
'agavi.environment' => (string)$environment, |
639
|
|
|
'agavi.context' => (string)$context, |
640
|
|
|
)); |
641
|
|
|
|
642
|
|
|
try { |
643
|
|
|
// transform the doc |
644
|
|
|
$newdoc = $proc->transformToDoc($document); |
645
|
|
|
} catch (\Exception $e) { |
646
|
|
|
throw new ParseException(sprintf('Configuration file "%s" could not be parsed: Could not transform the document using the XSL stylesheet "%s": %s', $document->documentURI, $xsl->documentURI, $e->getMessage()), 0, $e); |
647
|
|
|
} |
648
|
|
|
|
649
|
|
|
// no errors and we got a document back? excellent. this will be our new baby from now. time to kill the old one |
650
|
|
|
|
651
|
|
|
// get the old document URI |
652
|
|
|
$documentUri = $document->documentURI; |
653
|
|
|
|
654
|
|
|
// and assign the new document to the old one |
655
|
|
|
$document = $newdoc; |
656
|
|
|
|
657
|
|
|
// save the old document URI just in case |
658
|
|
|
$document->documentURI = $documentUri; |
659
|
|
|
} |
660
|
|
|
|
661
|
|
|
return $document; |
662
|
|
|
} |
663
|
|
|
|
664
|
|
|
/** |
665
|
|
|
* Transforms a given document according to xml-stylesheet processing |
666
|
|
|
* instructions |
667
|
|
|
* |
668
|
|
|
* @param XmlConfigDomDocument $document The document to act upon. |
669
|
|
|
* @param string $environment The environment name. |
670
|
|
|
* @param string $context The context name. |
671
|
|
|
* |
672
|
|
|
* @return XmlConfigDomDocument The transformed document. |
673
|
|
|
* |
674
|
|
|
* @author David Zülke <[email protected]> |
675
|
|
|
* @author Noah Fontes <[email protected]> |
676
|
|
|
* @since 1.0.0 |
677
|
|
|
*/ |
678
|
|
|
public static function transformProcessingInstructions(XmlConfigDomDocument $document, $environment, $context) |
679
|
|
|
{ |
680
|
|
|
$transformations = array(); |
681
|
|
|
$transformationInfo = array(); |
682
|
|
|
|
683
|
|
|
$xpath = $document->getXpath(); |
684
|
|
|
|
685
|
|
|
// see if there are <?xml-stylesheet... processing instructions |
686
|
|
|
$stylesheetProcessingInstructions = $xpath->query("//processing-instruction('xml-stylesheet')", $document); |
687
|
|
|
foreach ($stylesheetProcessingInstructions as $pi) { |
688
|
|
|
// yes! alright. trick: we create a doc fragment with the contents so we don't have to parse things by hand... |
689
|
|
|
$fragment = $document->createDocumentFragment(); |
690
|
|
|
$fragment->appendXml('<foo ' . $pi->data . ' />'); |
691
|
|
|
$type = $fragment->firstChild->getAttribute('type'); |
692
|
|
|
// we process only the types below... |
693
|
|
|
if (in_array($type, array('text/xml', 'text/xsl', 'application/xml', 'application/xsl+xml'))) { |
694
|
|
|
$href = $href = $fragment->firstChild->getAttribute('href'); |
695
|
|
|
|
696
|
|
|
if (strpos($href, '#') === 0) { |
697
|
|
|
// the href points to an embedded XSL stylesheet (with ID reference), so let's see if we can find it |
698
|
|
|
$stylesheets = $xpath->query("//*[@id='" . substr($href, 1) . "']", $document); |
699
|
|
|
if ($stylesheets->length) { |
700
|
|
|
// excellent. make a new doc from that element! |
701
|
|
|
try { |
702
|
|
|
$xsl = new XmlConfigDomDocument(); |
703
|
|
|
$xsl->appendChild($xsl->importNode($stylesheets->item(0), true)); |
704
|
|
|
} catch (\DOMException $dome) { |
705
|
|
|
throw new ParseException(sprintf('Configuration file "%s" could not be parsed: Could not load XSL stylesheet "%s": %s', $document->documentURI, $href, $dome->getMessage()), 0, $dome); |
706
|
|
|
} |
707
|
|
|
|
708
|
|
|
// and append to the list of XSLs to process |
709
|
|
|
// TODO: spec mandates that external XSLs be processed first! |
710
|
|
|
$transformations[] = $xsl; |
711
|
|
|
} else { |
712
|
|
|
throw new ParseException(sprintf('Configuration file "%s" could not be parsed because the inline stylesheet "%s" referenced in the "xml-stylesheet" processing instruction could not be found in the document.', $document->documentURI, $href)); |
713
|
|
|
} |
714
|
|
|
} else { |
715
|
|
|
// href references an xsl file, remember the path |
716
|
|
|
$transformationInfo[] = Toolkit::expandDirectives($href); |
717
|
|
|
} |
718
|
|
|
|
719
|
|
|
// remove the processing instructions after we dealt with them |
720
|
|
|
$pi->parentNode->removeChild($pi); |
721
|
|
|
} |
722
|
|
|
} |
723
|
|
|
|
724
|
|
|
return self::transform($document, $environment, $context, $transformationInfo, $transformations); |
725
|
|
|
} |
726
|
|
|
|
727
|
|
|
/** |
728
|
|
|
* Perform validation on a given document. |
729
|
|
|
* |
730
|
|
|
* @param XmlConfigDomDocument $document The document to act upon. |
731
|
|
|
* @param string $environment The environment name. |
732
|
|
|
* @param string $context The context name. |
733
|
|
|
* @param array $validationInfo An array of validation information. |
734
|
|
|
* |
735
|
|
|
* @author David Zülke <[email protected]> |
736
|
|
|
* @author Noah Fontes <[email protected]> |
737
|
|
|
* @since 0.11.0 |
738
|
|
|
*/ |
739
|
|
|
public static function validate(XmlConfigDomDocument $document, $environment, $context, array $validationInfo = array()) |
740
|
|
|
{ |
741
|
|
|
// bail out right away if validation is disabled |
742
|
|
|
if (Config::get('core.skip_config_validation', false)) { |
|
|
|
|
743
|
|
|
return; |
744
|
|
|
} |
745
|
|
|
|
746
|
|
|
$errors = array(); |
747
|
|
|
|
748
|
|
|
foreach ($validationInfo as $type => $files) { |
749
|
|
|
try { |
750
|
|
|
switch ($type) { |
751
|
|
|
case self::VALIDATION_TYPE_XMLSCHEMA: |
752
|
|
|
self::validateXmlschema($document, (array) $files); |
753
|
|
|
break; |
754
|
|
|
case self::VALIDATION_TYPE_RELAXNG: |
755
|
|
|
self::validateRelaxng($document, (array) $files); |
756
|
|
|
break; |
757
|
|
|
case self::VALIDATION_TYPE_SCHEMATRON: |
758
|
|
|
self::validateSchematron($document, $environment, $context, (array) $files); |
759
|
|
|
break; |
760
|
|
|
} |
761
|
|
|
} catch (ParseException $e) { |
762
|
|
|
$errors[] = $e->getMessage(); |
763
|
|
|
} |
764
|
|
|
} |
765
|
|
|
|
766
|
|
|
if ($errors) { |
|
|
|
|
767
|
|
|
throw new ParseException(sprintf('Validation of configuration file "%s" failed:' . "\n\n%s", $document->documentURI, implode("\n\n", $errors))); |
768
|
|
|
} |
769
|
|
|
} |
770
|
|
|
|
771
|
|
|
/** |
772
|
|
|
* Clean up a given document. |
773
|
|
|
* |
774
|
|
|
* @param XmlConfigDomDocument $document The document to clean up. |
775
|
|
|
* |
776
|
|
|
* @author David Zülke <[email protected]> |
777
|
|
|
* @since 0.11.0 |
778
|
|
|
*/ |
779
|
|
|
public static function cleanup(XmlConfigDomDocument $document) |
780
|
|
|
{ |
781
|
|
|
// remove top-level <sandbox> element |
782
|
|
|
if ($sandbox = $document->getSandbox()) { |
783
|
|
|
$sandbox->parentNode->removeChild($sandbox); |
784
|
|
|
} |
785
|
|
|
} |
786
|
|
|
|
787
|
|
|
/** |
788
|
|
|
* Validate a given document according to XMLSchema-instance (xsi) |
789
|
|
|
* declarations. |
790
|
|
|
* |
791
|
|
|
* @param XmlConfigDomDocument $document The document to act upon. |
792
|
|
|
* |
793
|
|
|
* @author David Zülke <[email protected]> |
794
|
|
|
* @author Noah Fontes <[email protected]> |
795
|
|
|
* @since 1.0.0 |
796
|
|
|
*/ |
797
|
|
|
public static function validateXsi(XmlConfigDomDocument $document) |
798
|
|
|
{ |
799
|
|
|
// next, find (and validate against) XML schema instance declarations |
800
|
|
|
$sources = array(); |
801
|
|
|
if ($document->documentElement->hasAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')) { |
802
|
|
|
// find locations. for namespaces, they are space separated pairs of a namespace URI and a schema location |
803
|
|
|
$locations = preg_split('/\s+/', $document->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')); |
804
|
|
|
for ($i = 1; $i < count($locations); $i = $i + 2) { |
|
|
|
|
805
|
|
|
$sources[] = $locations[$i]; |
806
|
|
|
} |
807
|
|
|
} |
808
|
|
|
// no namespace? then it's only one schema location in this attribute |
809
|
|
|
if ($document->documentElement->hasAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'noNamespaceSchemaLocation')) { |
810
|
|
|
$sources[] = $document->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'noNamespaceSchemaLocation'); |
811
|
|
|
} |
812
|
|
|
if ($sources) { |
|
|
|
|
813
|
|
|
// we have instances to validate against... |
814
|
|
|
$schemas = array(); |
815
|
|
|
foreach ($sources as &$source) { |
816
|
|
|
// so for each location, we need to grab the file and validate against this grabbed source code, as libxml often has a hard time retrieving stuff over HTTP |
817
|
|
|
$source = Toolkit::expandDirectives($source); |
818
|
|
|
if (parse_url($source, PHP_URL_SCHEME) === null && !Toolkit::isPathAbsolute($source)) { |
819
|
|
|
// the schema location is relative to the XML file |
820
|
|
|
$source = dirname($document->documentURI) . DIRECTORY_SEPARATOR . $source; |
821
|
|
|
} |
822
|
|
|
$schema = @file_get_contents($source); |
823
|
|
|
if ($schema === false) { |
824
|
|
|
throw new UnreadableException(sprintf('XML Schema validation file "%s" for configuration file "%s" does not exist or is unreadable', $source, $document->documentURI)); |
825
|
|
|
} |
826
|
|
|
$schemas[] = $schema; |
827
|
|
|
} |
828
|
|
|
// now validate them all |
829
|
|
|
self::validateXmlschemaSource($document, $schemas); |
830
|
|
|
} |
831
|
|
|
} |
832
|
|
|
|
833
|
|
|
/** |
834
|
|
|
* Validate the document against the given list of XML Schema files. |
835
|
|
|
* |
836
|
|
|
* @param XmlConfigDomDocument $document The document to act upon. |
837
|
|
|
* @param array $validationFiles An array of file names to validate against. |
838
|
|
|
* |
839
|
|
|
* @author David Zülke <[email protected]> |
840
|
|
|
* @author Noah Fontes <[email protected]> |
841
|
|
|
* @since 0.11.0 |
842
|
|
|
*/ |
843
|
|
View Code Duplication |
public static function validateXmlschema(XmlConfigDomDocument $document, array $validationFiles = array()) |
|
|
|
|
844
|
|
|
{ |
845
|
|
|
foreach ($validationFiles as $validationFile) { |
846
|
|
|
if (!is_resource($validationFile) && !is_readable($validationFile)) { |
847
|
|
|
throw new UnreadableException(sprintf('XML Schema validation file "%s" for configuration file "%s" does not exist or is unreadable', $validationFile, $document->documentURI)); |
848
|
|
|
} |
849
|
|
|
|
850
|
|
|
try { |
851
|
|
|
$document->schemaValidate($validationFile); |
852
|
|
|
} catch (\DOMException $dome) { |
853
|
|
|
throw new ParseException(sprintf('XML Schema validation of configuration file "%s" failed:' . "\n\n%s", $document->documentURI, $dome->getMessage()), 0, $dome); |
854
|
|
|
} |
855
|
|
|
} |
856
|
|
|
} |
857
|
|
|
|
858
|
|
|
/** |
859
|
|
|
* Validate the document against the given list of XML Schema documents. |
860
|
|
|
* |
861
|
|
|
* @param XmlConfigDomDocument $document The document to act upon. |
862
|
|
|
* @param array $validationSources An array of schema documents to validate against. |
863
|
|
|
* |
864
|
|
|
* @author David Zülke <[email protected]> |
865
|
|
|
* @author Noah Fontes <[email protected]> |
866
|
|
|
* @since 1.0.0 |
867
|
|
|
*/ |
868
|
|
|
public static function validateXmlschemaSource(XmlConfigDomDocument $document, array $validationSources = array()) |
869
|
|
|
{ |
870
|
|
|
foreach ($validationSources as $validationSource) { |
871
|
|
|
try { |
872
|
|
|
$document->schemaValidateSource($validationSource); |
873
|
|
|
} catch (\DOMException $dome) { |
874
|
|
|
throw new ParseException(sprintf('XML Schema validation of configuration file "%s" failed:' . "\n\n%s", $document->documentURI, $dome->getMessage()), 0, $dome); |
875
|
|
|
} |
876
|
|
|
} |
877
|
|
|
} |
878
|
|
|
|
879
|
|
|
/** |
880
|
|
|
* Validate the document against the given list of RELAX NG files. |
881
|
|
|
* |
882
|
|
|
* @param XmlConfigDomDocument $document The document to act upon. |
883
|
|
|
* @param array $validationFiles An array of file names to validate against. |
884
|
|
|
* |
885
|
|
|
* @author David Zülke <[email protected]> |
886
|
|
|
* @author Noah Fontes <[email protected]> |
887
|
|
|
* @since 0.11.0 |
888
|
|
|
*/ |
889
|
|
View Code Duplication |
public static function validateRelaxng(XmlConfigDomDocument $document, array $validationFiles = array()) |
|
|
|
|
890
|
|
|
{ |
891
|
|
|
foreach ($validationFiles as $validationFile) { |
892
|
|
|
if (!is_readable($validationFile)) { |
893
|
|
|
throw new UnreadableException(sprintf('RELAX NG validation file "%s" for configuration file "%s" does not exist or is unreadable', $validationFile, $document->documentURI)); |
894
|
|
|
} |
895
|
|
|
|
896
|
|
|
try { |
897
|
|
|
$document->relaxNGValidate($validationFile); |
898
|
|
|
} catch (\DOMException $dome) { |
899
|
|
|
throw new ParseException(sprintf('RELAX NG validation of configuration file "%s" failed:' . "\n\n%s", $document->documentURI, $dome->getMessage()), 0, $dome); |
900
|
|
|
} |
901
|
|
|
} |
902
|
|
|
} |
903
|
|
|
|
904
|
|
|
/** |
905
|
|
|
* Validate the document against the given list of Schematron files. |
906
|
|
|
* |
907
|
|
|
* @param XmlConfigDomDocument $document The document to act upon. |
908
|
|
|
* @param string $environment The environment name. |
909
|
|
|
* @param string $context The context name. |
910
|
|
|
* @param array $validationFiles An array of file names to validate against. |
911
|
|
|
* |
912
|
|
|
* @author David Zülke <[email protected]> |
913
|
|
|
* @author Noah Fontes <[email protected]> |
914
|
|
|
* @since 0.11.0 |
915
|
|
|
*/ |
916
|
|
|
public static function validateSchematron(XmlConfigDomDocument $document, $environment, $context, array $validationFiles = array()) |
917
|
|
|
{ |
918
|
|
|
if (Config::get('core.skip_config_transformations', false)) { |
|
|
|
|
919
|
|
|
return; |
920
|
|
|
} |
921
|
|
|
|
922
|
|
|
// load the schematron processor |
923
|
|
|
$schematron = new SchematronProcessor(); |
924
|
|
|
$schematron->setNode($document); |
925
|
|
|
// set some info (config file path, context name, environment name) as params |
926
|
|
|
// first arg is the namespace URI, which PHP doesn't support. awesome. see http://bugs.php.net/bug.php?id=30622 for the sad details |
927
|
|
|
// we could use "agavi:context" etc, that does work even without such a prefix being declared in the stylesheet, but that would be completely non-XML-ish, confusing, and against the spec. so we use dots instead. |
928
|
|
|
$schematron->setParameters(array( |
929
|
|
|
'agavi.config_path' => $document->documentURI, |
930
|
|
|
'agavi.environment' => $environment, |
931
|
|
|
'agavi.context' => $context, |
932
|
|
|
)); |
933
|
|
|
|
934
|
|
|
// loop over all validation files. those are .sch schematron schemas, which we transform to an XSL document that is then used to validate the source document :) |
935
|
|
|
foreach ($validationFiles as $href) { |
936
|
|
|
if (!is_readable($href)) { |
937
|
|
|
throw new UnreadableException(sprintf('Schematron validation file "%s" for configuration file "%s" does not exist or is unreadable', $href, $document->documentURI)); |
938
|
|
|
} |
939
|
|
|
|
940
|
|
|
// load the .sch file |
941
|
|
|
try { |
942
|
|
|
$sch = new XmlConfigDomDocument(); |
943
|
|
|
$sch->load($href); |
944
|
|
|
} catch (\DOMException $dome) { |
945
|
|
|
throw new ParseException(sprintf('Schematron validation of configuration file "%s" failed: Could not load schema file "%s": %s', $document->documentURI, $href, $dome->getMessage()), 0, $dome); |
946
|
|
|
} |
947
|
|
|
|
948
|
|
|
// perform the validation transformation |
949
|
|
|
try { |
950
|
|
|
$result = $schematron->transform($sch); |
951
|
|
|
} catch (\Exception $e) { |
952
|
|
|
throw new ParseException(sprintf('Schematron validation of configuration file "%s" failed: Transformation failed: %s', $document->documentURI, $e->getMessage()), 0, $e); |
953
|
|
|
} |
954
|
|
|
|
955
|
|
|
// validation ran okay, now we need to look at the result document to see if there are errors |
956
|
|
|
/** @var \DOMXPath $xpath */ |
957
|
|
|
$xpath = $result->getXpath(); |
958
|
|
|
$xpath->registerNamespace('svrl', self::NAMESPACE_SVRL_ISO); |
959
|
|
|
|
960
|
|
|
$results = $xpath->query('/svrl:schematron-output/svrl:failed-assert/svrl:text'); |
961
|
|
|
if ($results->length) { |
962
|
|
|
$errors = array('Failed assertions:'); |
963
|
|
|
|
964
|
|
|
foreach ($results as $result) { |
965
|
|
|
$errors[] = $result->nodeValue; |
966
|
|
|
} |
967
|
|
|
|
968
|
|
|
$results = $xpath->query('/svrl:schematron-output/svrl:successful-report/svrl:text'); |
969
|
|
|
if ($results->length) { |
970
|
|
|
$errors[] = ''; |
971
|
|
|
$errors[] = 'Successful reports:'; |
972
|
|
|
foreach ($results as $result) { |
973
|
|
|
$errors[] = $result->nodeValue; |
974
|
|
|
} |
975
|
|
|
} |
976
|
|
|
|
977
|
|
|
throw new ParseException(sprintf('Schematron validation of configuration file "%s" failed:' . "\n\n%s", $document->documentURI, implode("\n", $errors))); |
978
|
|
|
} |
979
|
|
|
} |
980
|
|
|
} |
981
|
|
|
} |
982
|
|
|
|
If you define a variable conditionally, it can happen that it is not defined for all execution paths.
Let’s take a look at an example:
In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.
Available Fixes
Check for existence of the variable explicitly:
Define a default value for the variable:
Add a value for the missing path: