XMLHelper::escape()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
1
<?php
2
/**
3
 * File containing the {@see AppUtils\XMLHelper} class.
4
 * 
5
 * @package Application Utils
6
 * @subpackage XMLHelper
7
 * @see XMLHelper
8
 */
9
10
declare(strict_types=1);
11
12
namespace AppUtils;
13
14
use DOMDocument;
15
use DOMException;
16
use DOMNode;
17
use DOMElement;
18
use SimpleXMLElement;
19
20
/**
21
 * Simple XML utility class that makes it easier to work
22
 * with the native PHP DOMDocument class. Simplifies the
23
 * code required to add common elements without going all
24
 * the way of being an abstraction layer.
25
 *
26
 * @package Application Utils
27
 * @subpackage XMLHelper
28
 * @author Sebastian Mordziol <[email protected]>
29
 */
30
class XMLHelper
31
{
32
    public const ERROR_CANNOT_APPEND_FRAGMENT = 491001; 
33
34
    private static bool $simulation = false;
35
    private DOMDocument $dom;
36
37
   /**
38
    * Creates a new XMLHelper instance.
39
    * 
40
    * @return XMLHelper
41
    */
42
    public static function create() : XMLHelper
43
    {
44
        $dom = new DOMDocument('1.0', 'UTF-8');
45
        $dom->formatOutput = true;
46
47
        return new XMLHelper($dom);
48
    }
49
50
    /**
51
     * Creates a converter instance from an XML file.
52
     * @param string $xmlFile
53
     * @return XMLHelper_Converter
54
     */
55
    public static function convertFile(string $xmlFile) : XMLHelper_Converter
56
    {
57
        return XMLHelper_Converter::fromFile($xmlFile);
58
    }
59
60
    /**
61
     * Creates a converter from an XML string.
62
     * @param string $xmlString
63
     * @return XMLHelper_Converter
64
     * @throws XMLHelper_Exception
65
     */
66
    public static function convertString(string $xmlString) : XMLHelper_Converter
67
    {
68
        return XMLHelper_Converter::fromString($xmlString);
69
    }
70
71
   /**
72
    * Creates a converter from a SimpleXMLElement instance.
73
    * @param SimpleXMLElement $element
74
    * @return XMLHelper_Converter
75
    */
76
    public static function convertElement(SimpleXMLElement $element) : XMLHelper_Converter
77
    {
78
        return XMLHelper_Converter::fromElement($element);
79
    }
80
   
81
   /**
82
    * Creates a converter from a DOMElement instance.
83
    * @param DOMElement $element
84
    * @return XMLHelper_Converter
85
    */
86
    public static function convertDOMElement(DOMElement $element) : XMLHelper_Converter
87
    {
88
        return XMLHelper_Converter::fromDOMElement($element);
89
    }
90
91
   /**
92
    * Creates a new helper using an existing DOMDocument object.
93
    * @param DOMDocument $dom
94
    */
95
    public function __construct(DOMDocument $dom)
96
    {
97
        $this->dom = $dom;
98
    }
99
100
   /**
101
    * @return DOMDocument
102
    */
103
    public function getDOM() : DOMDocument
104
    {
105
        return $this->dom;
106
    }
107
108
    /**
109
     * Adds an attribute to an existing tag with
110
     * the specified value.
111
     *
112
     * @param DOMNode $parent
113
     * @param string $name
114
     * @param mixed $value
115
     * @return DOMNode
116
     * @throws DOMException
117
     */
118
    public function addAttribute(DOMNode $parent, string $name, $value) : DOMNode
119
    {
120
        $node = $this->dom->createAttribute($name);
121
        $text = $this->dom->createTextNode((string)$value);
122
        $node->appendChild($text);
123
124
        return $parent->appendChild($node);
125
    }
126
127
    /**
128
     * Adds several attributes to the target node.
129
     *
130
     * @param DOMNode $parent
131
     * @param array<string,mixed> $attributes
132
     * @throws DOMException
133
     */
134
    public function addAttributes(DOMNode $parent, array $attributes) : void
135
    {
136
        foreach ($attributes as $name => $value) {
137
            $this->addAttribute($parent, $name, $value);
138
        }
139
    }
140
141
    /**
142
     * Adds a tag without content.
143
     *
144
     * @param DOMNode $parent
145
     * @param string $name
146
     * @param integer $indent
147
     * @return DOMNode
148
     * @throws DOMException
149
     */
150
    public function addTag(DOMNode $parent, string $name, int $indent = 0) : DOMNode
151
    {
152
        if ($indent > 0) {
153
            $this->indent($parent, $indent);
154
        }
155
156
        return $parent->appendChild(
157
            $this->dom->createElement($name)
158
        );
159
    }
160
161
    public function removeTag(DOMElement $tag) : void
162
    {
163
        if(isset($tag->parentNode))
164
        {
165
            $tag->parentNode->removeChild($tag);
0 ignored issues
show
Bug introduced by
The method removeChild() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

165
            $tag->parentNode->/** @scrutinizer ignore-call */ 
166
                              removeChild($tag);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
166
        }
167
    }
168
    
169
    public function indent(DOMNode $parent, int $amount) : void
170
    {
171
        $parent->appendChild($this->dom->createTextNode(str_repeat("\t", $amount)));
172
    }
173
174
    /**
175
     * Adds a tag with textual content, like:
176
     *
177
     * <tagname>text</tagname>
178
     *
179
     * @param DOMNode $parent
180
     * @param string $name
181
     * @param string $text
182
     * @param integer $indent
183
     * @return DOMNode
184
     * @throws DOMException
185
     */
186
    public function addTextTag(DOMNode $parent, string $name, string $text, int $indent = 0) : DOMNode
187
    {
188
        if ($indent > 0) {
189
            $this->indent($parent, $indent);
190
        }
191
192
        $tag = $this->dom->createElement($name);
193
        $tag->appendChild($this->dom->createTextNode($text));
194
195
        return $parent->appendChild($tag);
196
    }
197
198
    /**
199
     * Adds a tag with textual content, like:
200
     *
201
     * <tagname>text</tagname>
202
     *
203
     * and removes <p> tags
204
     *
205
     * @param DOMNode $parent
206
     * @param string $name
207
     * @param string $text
208
     * @param integer $indent
209
     * @return DOMNode
210
     * @throws DOMException
211
     */
212
    public function addEscapedTag(DOMNode $parent, string $name, string $text, int $indent = 0) : DOMNode
213
    {
214
        if ($indent > 0) {
215
            $this->indent($parent, $indent);
216
        }
217
218
        $text = preg_replace('#<p>(.*)</p>#isUm', '$1', $text);
219
220
        $tag = $this->dom->createElement($name);
221
        $tag->appendChild($this->dom->createTextNode($text));
222
223
        return $parent->appendChild($tag);
224
    }
225
226
    /**
227
     * Adds a tag with HTML content, like:
228
     *
229
     * <tagname><i>text</i></tagname>
230
     *
231
     * Tags will not be escaped.
232
     *
233
     * @param DOMNode $parent
234
     * @param string $name
235
     * @param string $text
236
     * @param integer $indent
237
     * @return DOMNode
238
     * @throws DOMException
239
     * @throws XMLHelper_Exception
240
     */
241
    public function addFragmentTag(DOMNode $parent, string $name, string $text, int $indent = 0) : DOMNode
242
    {
243
        if ($indent > 0) {
244
            $this->indent($parent, $indent);
245
        }
246
247
        $tag = $this->dom->createElement($name);
248
249
        if (!empty($text)) {
250
            $fragment = $this->dom->createDocumentFragment();
251
            if(!@$fragment->appendXML($text)) {
252
                throw new XMLHelper_Exception(
253
                    'Cannot append XML fragment',
254
                    sprintf(
255
                        'Appending text content to the fragment tag [%s] failed. Text content: [%s].',
256
                        $name,
257
                        htmlspecialchars($text, ENT_QUOTES, 'UTF-8')    
258
                    ),
259
                    self::ERROR_CANNOT_APPEND_FRAGMENT
260
                );
261
            }
262
            $tag->appendChild($fragment);
263
        }
264
265
        return $parent->appendChild($tag);
266
    }
267
268
    /**
269
     * Adds a tag with CDATA content, like:
270
     *
271
     * <tagname><![CDATA[value]]></tagname>
272
     *
273
     * @param DOMNode $parent
274
     * @param string $name
275
     * @param string $content
276
     * @return DOMNode
277
     * @throws DOMException
278
     */
279
    public function addCDATATag(DOMNode $parent, string $name, string $content) : DOMNode
280
    {
281
        $tag = $this->dom->createElement($name);
282
        $text = $this->dom->createCDATASection($content);
283
        $tag->appendChild($text);
284
285
        return $parent->appendChild($tag);
286
    }
287
288
    /**
289
     * Creates the root element of the document.
290
     * @param string $name
291
     * @param array<string,mixed> $attributes
292
     * @return DOMNode
293
     * @throws DOMException
294
     */
295
    public function createRoot(string $name, array $attributes=array()) : DOMNode
296
    {
297
        $root = $this->dom->appendChild($this->dom->createElement($name));
298
        $this->addAttributes($root, $attributes);
299
        return $root;
300
    }
301
302
   /**
303
    * Escaped the string for use in XML.
304
    * 
305
    * @param string $string
306
    * @return string
307
    */
308
    public function escape(string $string) : string
309
    {
310
        return preg_replace('#<p>(.*)</p>#isUm', '$1', $string);
311
    }
312
313
    public function escapeText(string $string) : string 
314
    {
315
        return str_replace(
316
            array(
317
                '&amp;',
318
                '&lt;',
319
                '&gt;',
320
                '&nbsp;',
321
                '&'
322
            ),
323
            array(
324
                'AMPERSAND_ESCAPE',
325
                'LT_ESCAPE',
326
                'GT_ESCAPE',
327
                ' ',
328
                '&amp;'
329
            ),
330
            $string
331
        );
332
    }
333
334
   /**
335
    * Sends the specified XML string to the browser with
336
    * the correct headers to trigger a download of the XML
337
    * to a local file.
338
    * 
339
    * NOTE: Ensure calling exit after this is done, and to
340
    * not send additional content, which would corrupt the 
341
    * download.
342
    *
343
    * @param string $xml
344
    * @param string $filename
345
    */
346
    public static function downloadXML(string $xml, string $filename = 'download.xml') : void
347
    {
348
        if(!self::$simulation && !headers_sent())
349
        {
350
            header('Content-Disposition: attachment; filename="' . $filename . '"');
351
        }
352
        
353
        echo $xml;
354
    }
355
356
   /**
357
    * Sends the specified XML string to the browser with
358
    * the correct headers and terminates the request.
359
    *
360
    * @param string $xml
361
    */
362
    public static function displayXML(string $xml) : void
363
    {
364
        if(!self::$simulation && !headers_sent())
365
        {
366
            header('Content-Type:text/xml; charset=utf-8');
367
        }
368
        
369
        if(self::$simulation) 
370
        {
371
            $xml = '<pre>'.htmlspecialchars($xml).'</pre>';
372
        }
373
        
374
        echo $xml;
375
    }
376
377
    /**
378
     * Shorthand method for building error xml and sending it
379
     * to the browser.
380
     *
381
     * @param string|int $code
382
     * @param string $message
383
     * @param string $title
384
     * @param array<string,string> $customInfo Associative array with name => value pairs for custom tags to add to the output xml
385
     * @throws DOMException
386
     * @see buildErrorXML()
387
     */
388
    public static function displayErrorXML($code, string $message, string $title, array $customInfo=array()) : void
389
    {
390
        if(!self::$simulation && !headers_sent()) {
391
            header('HTTP/1.1 400 Bad Request: ' . $title, true, 400);
392
        }
393
394
        self::displayXML(self::buildErrorXML($code, $message, $title, $customInfo));
395
    }
396
    
397
    public static function setSimulation(bool $simulate=true) : void
398
    {
399
        self::$simulation = $simulate;
400
    }
401
402
    /**
403
     * Creates XML markup to describe an application success
404
     * message when using XML-based services. Creates XML
405
     * with the following structure:
406
     *
407
     * <success>
408
     *     <message>Success message here</message>
409
     *     <time>YYYY-MM-DD HH:II:SS</time>
410
     * </success>
411
     *
412
     * @param string $message
413
     * @return string
414
     * @throws DOMException
415
     */
416
    public static function buildSuccessXML(string $message) : string
417
    {
418
        $xml = new DOMDocument('1.0', 'UTF-8');
419
        $xml->formatOutput = true;
420
421
        $helper = new XMLHelper($xml);
422
423
        $root = $helper->createRoot('success');
424
        $helper->addTextTag($root, 'message', $message);
425
        $helper->addTextTag($root, 'time', date('Y-m-d H:i:s'));
426
427
        return $xml->saveXML();
428
    }
429
430
    /**
431
     * Creates XML markup to describe an application error
432
     * when using XML services. Creates XML with the
433
     * following structure:
434
     *
435
     * <error>
436
     *     <id>99</id>
437
     *     <message>Full error message text</message>
438
     *     <title>Short error label</title>
439
     * </error>
440
     *
441
     * @param string|int $code
442
     * @param string $message
443
     * @param string $title
444
     * @param array<string,string> $customInfo
445
     * @return string
446
     * @throws DOMException
447
     */
448
    public static function buildErrorXML($code, string $message, string $title, array $customInfo=array()) : string
449
    {
450
        $xml = new DOMDocument('1.0', 'UTF-8');
451
        $xml->formatOutput = true;
452
453
        $helper = new XMLHelper($xml);
454
455
        $root = $helper->createRoot('error');
456
        
457
        $helper->addTextTag($root, 'id', $code);
458
        $helper->addTextTag($root, 'message', $message);
459
        $helper->addTextTag($root, 'title', $title);
460
        $helper->addTextTag($root, 'request_uri', $_SERVER['REQUEST_URI']);
461
        
462
        foreach($customInfo as $name => $value) {
463
            $helper->addTextTag($root, $name, $value);
464
        }
465
466
        return $xml->saveXML();
467
    }
468
469
    public function appendNewline(DOMNode $node) : void
470
    {
471
        $nl = $this->dom->createTextNode("\n");
472
        $node->appendChild($nl);
473
    }
474
475
    public function saveXML() : string
476
    {
477
        return $this->dom->saveXML();
478
    }
479
    
480
   /**
481
    * Creates a new SimpleXML helper instance: this
482
    * object is useful to work with loading XML strings
483
    * and files with easy access to any errors that 
484
    * may occurr, since the simplexml functions can be
485
    * somewhat cryptic.
486
    * 
487
    * @return XMLHelper_SimpleXML
488
    */
489
    public static function createSimplexml() : XMLHelper_SimpleXML
490
    {
491
        return new XMLHelper_SimpleXML();
492
    }
493
    
494
   /**
495
    * Converts a string to valid XML: can be a text only string
496
    * or an HTML string. Returns valid XML code.
497
    * 
498
    * NOTE: The string may contain custom tags, which are 
499
    * preserved.
500
    * 
501
    * @param string $string
502
    * @return string
503
    */
504
    public static function string2xml(string $string) : string
505
    {
506
        return XMLHelper_HTMLLoader::loadFragment($string)->fragmentToXML();
507
    }
508
}
509