Commit | Line | Data |
---|---|---|
60a35505 AE |
1 | <?php |
2 | namespace wcf\util; | |
df0d9cf6 | 3 | use wcf\system\exception\SystemException; |
60a35505 AE |
4 | |
5 | /** | |
6 | * Provides helper methods to work with PHP's DOM implementation. | |
7 | * | |
8ff2cd79 | 8 | * @author Alexander Ebert |
c839bd49 | 9 | * @copyright 2001-2018 WoltLab GmbH |
8ff2cd79 | 10 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> |
e71525e4 | 11 | * @package WoltLabSuite\Core\Util |
60a35505 AE |
12 | */ |
13 | final class DOMUtil { | |
14 | /** | |
15 | * Moves all child nodes from given element into a document fragment. | |
16 | * | |
8ff2cd79 MS |
17 | * @param \DOMElement $element element |
18 | * @return \DOMDocumentFragment document fragment containing all child nodes from `$element` | |
60a35505 AE |
19 | */ |
20 | public static function childNodesToFragment(\DOMElement $element) { | |
21 | $fragment = $element->ownerDocument->createDocumentFragment(); | |
22 | ||
23 | while ($element->hasChildNodes()) { | |
24 | $fragment->appendChild($element->childNodes->item(0)); | |
25 | } | |
26 | ||
27 | return $fragment; | |
28 | } | |
29 | ||
30 | /** | |
31 | * Returns true if `$ancestor` contains the node `$node`. | |
32 | * | |
8ff2cd79 MS |
33 | * @param \DOMNode $ancestor ancestor node |
34 | * @param \DOMNode $node node | |
35 | * @return boolean true if `$ancestor` contains the node `$node` | |
60a35505 AE |
36 | */ |
37 | public static function contains(\DOMNode $ancestor, \DOMNode $node) { | |
38 | // nodes cannot contain themselves | |
39 | if ($ancestor === $node) { | |
40 | return false; | |
41 | } | |
42 | ||
43 | // text nodes cannot contain any other nodes | |
44 | if ($ancestor->nodeType === XML_TEXT_NODE) { | |
45 | return false; | |
46 | } | |
47 | ||
48 | $parent = $node; | |
49 | while ($parent = $parent->parentNode) { | |
50 | if ($parent === $ancestor) { | |
51 | return true; | |
52 | } | |
53 | } | |
54 | ||
55 | return false; | |
56 | } | |
57 | ||
1f9d08af AE |
58 | /** |
59 | * Returns a static list of child nodes of provided element. | |
60 | * | |
61 | * @param \DOMElement $element target element | |
62 | * @return \DOMNode[] list of child nodes | |
63 | */ | |
64 | public static function getChildNodes(\DOMElement $element) { | |
65 | $nodes = []; | |
66 | foreach ($element->childNodes as $node) { | |
67 | $nodes[] = $node; | |
68 | } | |
69 | ||
70 | return $nodes; | |
71 | } | |
72 | ||
60a35505 AE |
73 | /** |
74 | * Returns the common ancestor of both nodes. | |
75 | * | |
8ff2cd79 MS |
76 | * @param \DOMNode $node1 first node |
77 | * @param \DOMNode $node2 second node | |
4be0ecec | 78 | * @return \DOMNode|null common ancestor or null |
60a35505 AE |
79 | */ |
80 | public static function getCommonAncestor(\DOMNode $node1, \DOMNode $node2) { | |
81 | // abort if both elements share a common element or are both direct descendants | |
82 | // of the same document | |
83 | if ($node1->parentNode === $node2->parentNode) { | |
84 | return $node1->parentNode; | |
85 | } | |
86 | ||
87 | // collect the list of all direct ancestors of `$node1` | |
88 | $parents = self::getParents($node1); | |
89 | ||
90 | // compare each ancestor of `$node2` to the known list of parents of `$node1` | |
91 | $parent = $node2; | |
92 | while ($parent = $parent->parentNode) { | |
93 | // requires strict type check | |
94 | if (in_array($parent, $parents, true)) { | |
95 | return $parent; | |
96 | } | |
97 | } | |
98 | ||
99 | return null; | |
100 | } | |
101 | ||
df0d9cf6 AE |
102 | /** |
103 | * Returns a non-live collection of elements. | |
104 | * | |
105 | * @param (\DOMDocument|\DOMElement) $context context element | |
106 | * @param string $tagName tag name | |
107 | * @return \DOMElement[] list of elements | |
108 | * @throws SystemException | |
109 | */ | |
110 | public static function getElements($context, $tagName) { | |
111 | if (!($context instanceof \DOMDocument) && !($context instanceof \DOMElement)) { | |
112 | throw new SystemException("Expected context to be either of type \\DOMDocument or \\DOMElement."); | |
113 | } | |
114 | ||
115 | $elements = []; | |
116 | foreach ($context->getElementsByTagName($tagName) as $element) { | |
117 | $elements[] = $element; | |
118 | } | |
119 | ||
120 | return $elements; | |
121 | } | |
122 | ||
60a35505 | 123 | /** |
b2aa772d | 124 | * Returns the immediate parent element before provided ancestor element. Returns null if |
60a35505 AE |
125 | * the ancestor element is the direct parent of provided node. |
126 | * | |
8ff2cd79 MS |
127 | * @param \DOMNode $node node |
128 | * @param \DOMElement $ancestor ancestor node | |
129 | * @return \DOMElement|null immediate parent element before ancestor element | |
60a35505 AE |
130 | */ |
131 | public static function getParentBefore(\DOMNode $node, \DOMElement $ancestor) { | |
132 | if ($node->parentNode === $ancestor) { | |
133 | return null; | |
134 | } | |
135 | ||
136 | $parents = self::getParents($node); | |
137 | for ($i = count($parents) - 1; $i >= 0; $i--) { | |
138 | if ($parents[$i] === $ancestor) { | |
139 | return $parents[$i - 1]; | |
140 | } | |
141 | } | |
142 | ||
143 | throw new \InvalidArgumentException("Provided node is a not a descendant of ancestor element."); | |
144 | } | |
145 | ||
146 | /** | |
147 | * Returns the parent node of given node. | |
148 | * | |
8ff2cd79 MS |
149 | * @param \DOMNode $node node |
150 | * @return \DOMNode parent node, can be `\DOMElement` or `\DOMDocument` | |
60a35505 AE |
151 | */ |
152 | public static function getParentNode(\DOMNode $node) { | |
63b9817b | 153 | return $node->parentNode ?: $node->ownerDocument; |
60a35505 AE |
154 | } |
155 | ||
156 | /** | |
157 | * Returns all ancestors nodes for given node. | |
158 | * | |
8ff2cd79 MS |
159 | * @param \DOMNode $node node |
160 | * @param boolean $reverseOrder reversing the order causes the most top ancestor to appear first | |
161 | * @return \DOMElement[] list of ancestor nodes | |
60a35505 AE |
162 | */ |
163 | public static function getParents(\DOMNode $node, $reverseOrder = false) { | |
164 | $parents = []; | |
165 | ||
166 | $parent = $node; | |
167 | while ($parent = $parent->parentNode) { | |
168 | $parents[] = $parent; | |
169 | } | |
170 | ||
63b9817b | 171 | return $reverseOrder ? array_reverse($parents) : $parents; |
60a35505 AE |
172 | } |
173 | ||
b615c818 AE |
174 | /** |
175 | * Returns a cloned parent tree that is virtually readonly. In fact it can be | |
176 | * modified, but all changes are non permanent and do not affect the source | |
177 | * document at all. | |
178 | * | |
179 | * @param \DOMNode $node node | |
180 | * @return \DOMElement[] list of parent elements | |
181 | */ | |
182 | public static function getReadonlyParentTree(\DOMNode $node) { | |
183 | $tree = []; | |
184 | /** @var \DOMElement $parent */ | |
185 | foreach (self::getParents($node) as $parent) { | |
186 | // do not include <body>, <html> and the document itself | |
187 | if ($parent->nodeName === 'body') break; | |
188 | ||
189 | $tree[] = $parent->cloneNode(false); | |
190 | } | |
191 | ||
192 | return $tree; | |
193 | } | |
194 | ||
60a35505 AE |
195 | /** |
196 | * Determines the relative position of two nodes to each other. | |
197 | * | |
8ff2cd79 MS |
198 | * @param \DOMNode $node1 first node |
199 | * @param \DOMNode $node2 second node | |
200 | * @return string | |
60a35505 AE |
201 | */ |
202 | public static function getRelativePosition(\DOMNode $node1, \DOMNode $node2) { | |
203 | if ($node1->ownerDocument !== $node2->ownerDocument) { | |
204 | throw new \InvalidArgumentException("Both nodes must be contained in the same DOM document."); | |
205 | } | |
206 | ||
207 | $nodeList1 = self::getParents($node1, true); | |
208 | $nodeList1[] = $node1; | |
209 | ||
210 | $nodeList2 = self::getParents($node2, true); | |
211 | $nodeList2[] = $node2; | |
212 | ||
60a35505 AE |
213 | $i = 0; |
214 | while ($nodeList1[$i] === $nodeList2[$i]) { | |
215 | $i++; | |
216 | } | |
217 | ||
218 | // check if parent of node 2 appears before parent of node 1 | |
219 | $previousSibling = $nodeList1[$i]; | |
220 | while ($previousSibling = $previousSibling->previousSibling) { | |
221 | if ($previousSibling === $nodeList2[$i]) { | |
222 | return 'before'; | |
223 | } | |
224 | } | |
225 | ||
226 | $nextSibling = $nodeList1[$i]; | |
227 | while ($nextSibling = $nextSibling->nextSibling) { | |
228 | if ($nextSibling === $nodeList2[$i]) { | |
229 | return 'after'; | |
230 | } | |
231 | } | |
232 | ||
d429281c | 233 | throw new \RuntimeException("Unable to determine relative node position."); |
60a35505 AE |
234 | } |
235 | ||
aa141dc6 AE |
236 | /** |
237 | * Returns true if there is at least one parent with the provided tag name. | |
238 | * | |
239 | * @param \DOMElement $element start element | |
240 | * @param string $tagName tag name to match | |
f27e5de2 | 241 | * @return boolean |
aa141dc6 AE |
242 | */ |
243 | public static function hasParent(\DOMElement $element, $tagName) { | |
244 | while ($element = $element->parentNode) { | |
245 | if ($element->nodeName === $tagName) { | |
246 | return true; | |
247 | } | |
248 | } | |
249 | ||
250 | return false; | |
251 | } | |
252 | ||
60a35505 AE |
253 | /** |
254 | * Inserts given DOM node after the reference node. | |
255 | * | |
8ff2cd79 MS |
256 | * @param \DOMNode $node node |
257 | * @param \DOMNode $refNode reference node | |
60a35505 AE |
258 | */ |
259 | public static function insertAfter(\DOMNode $node, \DOMNode $refNode) { | |
260 | if ($refNode->nextSibling) { | |
261 | self::insertBefore($node, $refNode->nextSibling); | |
262 | } | |
263 | else { | |
264 | self::getParentNode($refNode)->appendChild($node); | |
265 | } | |
266 | } | |
267 | ||
268 | /** | |
269 | * Inserts given node before the reference node. | |
270 | * | |
8ff2cd79 MS |
271 | * @param \DOMNode $node node |
272 | * @param \DOMNode $refNode reference node | |
60a35505 AE |
273 | */ |
274 | public static function insertBefore(\DOMNode $node, \DOMNode $refNode) { | |
275 | self::getParentNode($refNode)->insertBefore($node, $refNode); | |
276 | } | |
277 | ||
d429281c AE |
278 | /** |
279 | * Returns true if this node is empty. | |
280 | * | |
8ff2cd79 MS |
281 | * @param \DOMNode $node node |
282 | * @return boolean true if node is empty | |
d429281c AE |
283 | */ |
284 | public static function isEmpty(\DOMNode $node) { | |
285 | if ($node->nodeType === XML_TEXT_NODE) { | |
286 | return (StringUtil::trim($node->nodeValue) === ''); | |
287 | } | |
288 | else if ($node->nodeType === XML_ELEMENT_NODE) { | |
289 | /** @var \DOMElement $node */ | |
290 | if (self::isVoidElement($node)) { | |
291 | return false; | |
292 | } | |
293 | else if ($node->hasChildNodes()) { | |
294 | for ($i = 0, $length = $node->childNodes->length; $i < $length; $i++) { | |
e6ff01e9 | 295 | if (!self::isEmpty($node->childNodes->item($i))) { |
d429281c AE |
296 | return false; |
297 | } | |
298 | } | |
299 | } | |
300 | ||
301 | return true; | |
302 | } | |
303 | ||
304 | return true; | |
305 | } | |
306 | ||
60a35505 AE |
307 | /** |
308 | * Returns true if given node is the first node of its given ancestor. | |
309 | * | |
8ff2cd79 MS |
310 | * @param \DOMNode $node node |
311 | * @param \DOMElement $ancestor ancestor element | |
312 | * @return boolean true if `$node` is the first node of its given ancestor | |
60a35505 AE |
313 | */ |
314 | public static function isFirstNode(\DOMNode $node, \DOMElement $ancestor) { | |
315 | if ($node->previousSibling === null) { | |
d429281c | 316 | if ($node->parentNode === $ancestor || $node->parentNode->nodeName === 'body') { |
60a35505 AE |
317 | return true; |
318 | } | |
319 | else { | |
320 | return self::isFirstNode($node->parentNode, $ancestor); | |
321 | } | |
322 | } | |
323 | else if ($node->parentNode->nodeName === 'body') { | |
324 | return true; | |
325 | } | |
326 | ||
327 | return false; | |
328 | } | |
329 | ||
330 | /** | |
331 | * Returns true if given node is the last node of its given ancestor. | |
332 | * | |
8ff2cd79 MS |
333 | * @param \DOMNode $node node |
334 | * @param \DOMElement $ancestor ancestor element | |
335 | * @return boolean true if `$node` is the last node of its given ancestor | |
60a35505 AE |
336 | */ |
337 | public static function isLastNode(\DOMNode $node, \DOMElement $ancestor) { | |
338 | if ($node->nextSibling === null) { | |
339 | if ($node->parentNode === null) { | |
340 | throw new \InvalidArgumentException("Provided node is a not a descendant of ancestor element."); | |
341 | } | |
342 | else if ($node->parentNode === $ancestor || $node->parentNode->nodeName === 'body') { | |
343 | return true; | |
344 | } | |
345 | else { | |
346 | return self::isLastNode($node->parentNode, $ancestor); | |
347 | } | |
348 | } | |
349 | else if ($node->parentNode->nodeName === 'body') { | |
350 | return true; | |
351 | } | |
352 | ||
353 | return false; | |
354 | } | |
355 | ||
cfbc1344 AE |
356 | /** |
357 | * Nodes can get partially destroyed in which they're still an | |
358 | * actual DOM node (such as \DOMElement) but almost their entire | |
359 | * body is gone, including the `nodeType` attribute. | |
360 | * | |
361 | * @param \DOMNode $node node | |
362 | * @return boolean true if node has been destroyed | |
363 | */ | |
364 | public static function isRemoved(\DOMNode $node) { | |
365 | return !isset($node->nodeType); | |
366 | } | |
367 | ||
d429281c AE |
368 | /** |
369 | * Returns true if provided element is a void element. Void elements are elements | |
370 | * that neither contain content nor have a closing tag, such as `<br>`. | |
371 | * | |
8ff2cd79 MS |
372 | * @param \DOMElement $element element |
373 | * @return boolean true if provided element is a void element | |
d429281c AE |
374 | */ |
375 | public static function isVoidElement(\DOMElement $element) { | |
376 | if (preg_match('~^(area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$~', $element->nodeName)) { | |
377 | return true; | |
378 | } | |
379 | ||
380 | return false; | |
381 | } | |
382 | ||
60a35505 AE |
383 | /** |
384 | * Moves all nodes into `$container` until it reaches `$lastElement`. The direction | |
385 | * in which nodes will be considered for moving is determined by the logical position | |
386 | * of `$lastElement`. | |
387 | * | |
8ff2cd79 MS |
388 | * @param \DOMElement $container destination element |
389 | * @param \DOMElement $lastElement last element to move | |
390 | * @param \DOMElement $commonAncestor common ancestor of `$container` and `$lastElement` | |
60a35505 AE |
391 | */ |
392 | public static function moveNodesInto(\DOMElement $container, \DOMElement $lastElement, \DOMElement $commonAncestor) { | |
393 | if (!self::contains($commonAncestor, $container)) { | |
394 | throw new \InvalidArgumentException("The container element must be a child of the common ancestor element."); | |
395 | } | |
396 | else if ($lastElement->parentNode !== $commonAncestor) { | |
397 | throw new \InvalidArgumentException("The last element must be a direct child of the common ancestor element."); | |
398 | } | |
399 | ||
400 | $relativePosition = self::getRelativePosition($container, $lastElement); | |
401 | ||
402 | // move everything that is logically after `$container` but within | |
403 | // `$commonAncestor` into `$container` until `$lastElement` has been moved | |
404 | $element = $container; | |
405 | do { | |
406 | if ($relativePosition === 'before') { | |
407 | while ($sibling = $element->previousSibling) { | |
408 | self::prepend($sibling, $container); | |
409 | if ($sibling === $lastElement) { | |
410 | return; | |
411 | } | |
412 | } | |
413 | } | |
414 | else { | |
415 | while ($sibling = $element->nextSibling) { | |
416 | $container->appendChild($sibling); | |
417 | if ($sibling === $lastElement) { | |
418 | return; | |
419 | } | |
420 | } | |
421 | } | |
422 | ||
423 | $element = $element->parentNode; | |
424 | } | |
425 | while ($element !== $commonAncestor); | |
426 | } | |
427 | ||
1f5d6093 AE |
428 | /** |
429 | * Normalizes an element by joining adjacent text nodes. | |
430 | * | |
431 | * @param \DOMElement $element target element | |
432 | */ | |
433 | public static function normalize(\DOMElement $element) { | |
ed0f1bdb | 434 | $childNodes = self::getChildNodes($element); |
1f5d6093 AE |
435 | /** @var \DOMNode $lastTextNode */ |
436 | $lastTextNode = null; | |
437 | foreach ($childNodes as $childNode) { | |
438 | if ($childNode->nodeType !== XML_TEXT_NODE) { | |
439 | $lastTextNode = null; | |
440 | continue; | |
441 | } | |
442 | ||
443 | if ($lastTextNode === null) { | |
444 | $lastTextNode = $childNode; | |
445 | } | |
446 | else { | |
447 | // merge with last text node | |
448 | $newTextNode = $childNode->ownerDocument->createTextNode($lastTextNode->textContent . $childNode->textContent); | |
449 | $element->insertBefore($newTextNode, $lastTextNode); | |
450 | ||
451 | $element->removeChild($lastTextNode); | |
452 | $element->removeChild($childNode); | |
453 | ||
454 | $lastTextNode = $newTextNode; | |
455 | } | |
456 | } | |
457 | } | |
458 | ||
60a35505 AE |
459 | /** |
460 | * Prepends a node to provided element. | |
461 | * | |
8ff2cd79 MS |
462 | * @param \DOMNode $node node |
463 | * @param \DOMElement $element target element | |
60a35505 AE |
464 | */ |
465 | public static function prepend(\DOMNode $node, \DOMElement $element) { | |
466 | if ($element->firstChild === null) { | |
467 | $element->appendChild($node); | |
468 | } | |
469 | else { | |
470 | $element->insertBefore($node, $element->firstChild); | |
471 | } | |
472 | } | |
473 | ||
474 | /** | |
475 | * Removes a node, optionally preserves the child nodes if `$node` is an element. | |
476 | * | |
8ff2cd79 MS |
477 | * @param \DOMNode $node target node |
478 | * @param boolean $preserveChildNodes preserve child nodes, only supported for elements | |
60a35505 AE |
479 | */ |
480 | public static function removeNode(\DOMNode $node, $preserveChildNodes = false) { | |
481 | if ($preserveChildNodes) { | |
482 | if (!($node instanceof \DOMElement)) { | |
483 | throw new \InvalidArgumentException("Preserving child nodes is only supported for DOMElement."); | |
484 | } | |
485 | ||
486 | while ($node->hasChildNodes()) { | |
487 | self::insertBefore($node->childNodes->item(0), $node); | |
488 | } | |
489 | } | |
490 | ||
491 | self::getParentNode($node)->removeChild($node); | |
492 | } | |
493 | ||
494 | /** | |
495 | * Replaces a DOM element with another, preserving all child nodes by default. | |
496 | * | |
8ff2cd79 MS |
497 | * @param \DOMElement $oldElement old element |
498 | * @param \DOMElement $newElement new element | |
499 | * @param boolean $preserveChildNodes true if child nodes should be moved, otherwise they'll be implicitly removed | |
60a35505 AE |
500 | */ |
501 | public static function replaceElement(\DOMElement $oldElement, \DOMElement $newElement, $preserveChildNodes = true) { | |
502 | self::insertBefore($newElement, $oldElement); | |
503 | ||
504 | // move all child nodes | |
505 | if ($preserveChildNodes) { | |
506 | while ($oldElement->hasChildNodes()) { | |
507 | $newElement->appendChild($oldElement->childNodes->item(0)); | |
508 | } | |
509 | } | |
510 | ||
511 | // remove old element | |
512 | self::getParentNode($oldElement)->removeChild($oldElement); | |
513 | } | |
514 | ||
515 | /** | |
516 | * Splits all parent nodes until `$ancestor` and moved other nodes after/before | |
517 | * (determined by `$splitBefore`) into the newly created nodes. This allows | |
518 | * extraction of DOM parts while preserving nesting for both the extracted nodes | |
519 | * and the remaining siblings. | |
520 | * | |
8ff2cd79 MS |
521 | * @param \DOMNode $node reference node |
522 | * @param \DOMElement $ancestor ancestor element that should not be split | |
523 | * @param boolean $splitBefore true if nodes before `$node` should be moved into a new node, false to split nodes after `$node` | |
4be0ecec | 524 | * @return \DOMNode parent node containing `$node`, direct child of `$ancestor` |
60a35505 AE |
525 | */ |
526 | public static function splitParentsUntil(\DOMNode $node, \DOMElement $ancestor, $splitBefore = true) { | |
527 | if (!self::contains($ancestor, $node)) { | |
528 | throw new \InvalidArgumentException("Node is not contained in ancestor node."); | |
529 | } | |
530 | ||
531 | // clone the parent node right "below" `$ancestor` | |
532 | $cloneNode = self::getParentBefore($node, $ancestor); | |
533 | ||
534 | if ($splitBefore) { | |
d85b1843 AE |
535 | if ($cloneNode === null) { |
536 | // target node is already a direct descendant of the ancestor | |
537 | // node, no need to split anything | |
538 | return $node; | |
539 | } | |
540 | else if (self::isFirstNode($node, $cloneNode)) { | |
60a35505 AE |
541 | // target node is at the very start, we can safely move the |
542 | // entire parent node around | |
543 | return $cloneNode; | |
544 | } | |
545 | ||
546 | $currentNode = $node; | |
547 | while (($parent = $currentNode->parentNode) !== $ancestor) { | |
325eb3e3 | 548 | /** @var \DOMElement $newNode */ |
60a35505 AE |
549 | $newNode = $parent->cloneNode(); |
550 | self::insertBefore($newNode, $parent); | |
551 | ||
552 | while ($currentNode->previousSibling) { | |
325eb3e3 | 553 | self::prepend($currentNode->previousSibling, $newNode); |
60a35505 AE |
554 | } |
555 | ||
556 | $currentNode = $parent; | |
557 | } | |
558 | } | |
559 | else { | |
d85b1843 AE |
560 | if ($cloneNode === null) { |
561 | // target node is already a direct descendant of the ancestor | |
562 | // node, no need to split anything | |
563 | return $node; | |
564 | } | |
565 | else if (self::isLastNode($node, $cloneNode)) { | |
60a35505 AE |
566 | // target node is at the very end, we can safely move the |
567 | // entire parent node around | |
568 | return $cloneNode; | |
569 | } | |
570 | ||
571 | $currentNode = $node; | |
572 | while (($parent = $currentNode->parentNode) !== $ancestor) { | |
573 | $newNode = $parent->cloneNode(); | |
574 | self::insertAfter($newNode, $parent); | |
575 | ||
576 | while ($currentNode->nextSibling) { | |
577 | $newNode->appendChild($currentNode->nextSibling); | |
578 | } | |
579 | ||
580 | $currentNode = $parent; | |
581 | } | |
582 | } | |
583 | ||
584 | return self::getParentBefore($node, $ancestor); | |
585 | } | |
586 | ||
1d5f9363 MS |
587 | /** |
588 | * Forbid creation of DOMUtil objects. | |
589 | */ | |
590 | private function __construct() { | |
591 | // does nothing | |
592 | } | |
60a35505 | 593 | } |