├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── checks.yml ├── .gitignore ├── cache └── .gitkeep ├── composer.json ├── composer.lock ├── licence.md ├── phpcs.xml.dist ├── phpstan.neon.dist ├── readme.md ├── src └── Xml │ ├── XmlException.php │ └── XmlLoader.php └── tests └── XmlLoaderTest.phpt /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 4 8 | 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - 4 | package-ecosystem: composer 5 | directory: "/" 6 | schedule: 7 | interval: monthly 8 | versioning-strategy: lockfile-only 9 | groups: 10 | dev-dependencies: 11 | dependency-type: development 12 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - "master" 7 | - "v[0-9]" 8 | jobs: 9 | checks: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | php-version: [ '8.0', '8.1', '8.2', '8.3', '8.4' ] 15 | steps: 16 | - 17 | name: Checkout code 18 | uses: actions/checkout@v4 19 | - 20 | name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: ${{ matrix.php-version }} 24 | - 25 | name: Install dependencies 26 | run: composer install --no-progress --prefer-dist --no-interaction 27 | - 28 | name: Run checks and tests 29 | run: composer check 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /cache/* 3 | !/cache/.gitkeep 4 | /.idea 5 | /tests/output 6 | -------------------------------------------------------------------------------- /cache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightools/xml/e1092d52ee07c2d0567c5f3dac1ef71e31436bd8/cache/.gitkeep -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lightools/xml", 3 | "description": "Simple and safe parsing of XML and HTML sources.", 4 | "license": ["MIT"], 5 | "require": { 6 | "php": ">=8.0", 7 | "ext-dom": "*", 8 | "ext-libxml": "*" 9 | }, 10 | "require-dev": { 11 | "editorconfig-checker/editorconfig-checker": "^10.4.0", 12 | "nette/tester": "^v2.5.1", 13 | "phpstan/phpstan": "^1.10.34", 14 | "phpstan/phpstan-strict-rules": "^1.5.1", 15 | "slevomat/coding-standard": "^8.13.4" 16 | }, 17 | "config": { 18 | "sort-packages": true, 19 | "allow-plugins": { 20 | "dealerdirect/phpcodesniffer-composer-installer": false 21 | } 22 | }, 23 | "autoload": { 24 | "classmap": ["src/"] 25 | }, 26 | "scripts": { 27 | "check": "ec && phpcs && phpstan analyse -vvv && tester -C tests" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "1a952dfc986e01ff06fafcd6bb1a2c47", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "dealerdirect/phpcodesniffer-composer-installer", 12 | "version": "v1.0.0", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/PHPCSStandards/composer-installer.git", 16 | "reference": "4be43904336affa5c2f70744a348312336afd0da" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", 21 | "reference": "4be43904336affa5c2f70744a348312336afd0da", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "composer-plugin-api": "^1.0 || ^2.0", 26 | "php": ">=5.4", 27 | "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" 28 | }, 29 | "require-dev": { 30 | "composer/composer": "*", 31 | "ext-json": "*", 32 | "ext-zip": "*", 33 | "php-parallel-lint/php-parallel-lint": "^1.3.1", 34 | "phpcompatibility/php-compatibility": "^9.0", 35 | "yoast/phpunit-polyfills": "^1.0" 36 | }, 37 | "type": "composer-plugin", 38 | "extra": { 39 | "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" 44 | } 45 | }, 46 | "notification-url": "https://packagist.org/downloads/", 47 | "license": [ 48 | "MIT" 49 | ], 50 | "authors": [ 51 | { 52 | "name": "Franck Nijhof", 53 | "email": "franck.nijhof@dealerdirect.com", 54 | "homepage": "http://www.frenck.nl", 55 | "role": "Developer / IT Manager" 56 | }, 57 | { 58 | "name": "Contributors", 59 | "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" 60 | } 61 | ], 62 | "description": "PHP_CodeSniffer Standards Composer Installer Plugin", 63 | "homepage": "http://www.dealerdirect.com", 64 | "keywords": [ 65 | "PHPCodeSniffer", 66 | "PHP_CodeSniffer", 67 | "code quality", 68 | "codesniffer", 69 | "composer", 70 | "installer", 71 | "phpcbf", 72 | "phpcs", 73 | "plugin", 74 | "qa", 75 | "quality", 76 | "standard", 77 | "standards", 78 | "style guide", 79 | "stylecheck", 80 | "tests" 81 | ], 82 | "support": { 83 | "issues": "https://github.com/PHPCSStandards/composer-installer/issues", 84 | "source": "https://github.com/PHPCSStandards/composer-installer" 85 | }, 86 | "time": "2023-01-05T11:28:13+00:00" 87 | }, 88 | { 89 | "name": "editorconfig-checker/editorconfig-checker", 90 | "version": "10.7.0", 91 | "source": { 92 | "type": "git", 93 | "url": "https://github.com/editorconfig-checker/editorconfig-checker.php.git", 94 | "reference": "eb2581bee39d10e776e69048a88fb10a76a0cc9f" 95 | }, 96 | "dist": { 97 | "type": "zip", 98 | "url": "https://api.github.com/repos/editorconfig-checker/editorconfig-checker.php/zipball/eb2581bee39d10e776e69048a88fb10a76a0cc9f", 99 | "reference": "eb2581bee39d10e776e69048a88fb10a76a0cc9f", 100 | "shasum": "" 101 | }, 102 | "require": { 103 | "php": "^7.2 || ^8.0" 104 | }, 105 | "require-dev": { 106 | "php-coveralls/php-coveralls": "^2.1", 107 | "phpstan/phpstan": "^1.4", 108 | "phpunit/phpunit": "^8.5.23", 109 | "squizlabs/php_codesniffer": "^3.6" 110 | }, 111 | "bin": [ 112 | "bin/editorconfig-checker", 113 | "bin/ec" 114 | ], 115 | "type": "library", 116 | "autoload": { 117 | "psr-4": { 118 | "EditorconfigChecker\\": "src/EditorconfigChecker" 119 | } 120 | }, 121 | "notification-url": "https://packagist.org/downloads/", 122 | "license": [ 123 | "MIT" 124 | ], 125 | "authors": [ 126 | { 127 | "name": "Max Strübing", 128 | "email": "mxstrbng@gmail.com", 129 | "homepage": "https://github.com/mstruebing", 130 | "role": "Maintainer" 131 | } 132 | ], 133 | "description": "A tool to verify that your files follow the rules of your .editorconfig", 134 | "support": { 135 | "issues": "https://github.com/editorconfig-checker/editorconfig-checker.php/issues", 136 | "source": "https://github.com/editorconfig-checker/editorconfig-checker.php" 137 | }, 138 | "time": "2025-03-17T14:59:08+00:00" 139 | }, 140 | { 141 | "name": "nette/tester", 142 | "version": "v2.5.4", 143 | "source": { 144 | "type": "git", 145 | "url": "https://github.com/nette/tester.git", 146 | "reference": "c11863785779e87b40adebf150364f2e5938c111" 147 | }, 148 | "dist": { 149 | "type": "zip", 150 | "url": "https://api.github.com/repos/nette/tester/zipball/c11863785779e87b40adebf150364f2e5938c111", 151 | "reference": "c11863785779e87b40adebf150364f2e5938c111", 152 | "shasum": "" 153 | }, 154 | "require": { 155 | "php": "8.0 - 8.4" 156 | }, 157 | "require-dev": { 158 | "ext-simplexml": "*", 159 | "phpstan/phpstan": "^1.0" 160 | }, 161 | "bin": [ 162 | "src/tester" 163 | ], 164 | "type": "library", 165 | "extra": { 166 | "branch-alias": { 167 | "dev-master": "2.5-dev" 168 | } 169 | }, 170 | "autoload": { 171 | "classmap": [ 172 | "src/" 173 | ] 174 | }, 175 | "notification-url": "https://packagist.org/downloads/", 176 | "license": [ 177 | "BSD-3-Clause", 178 | "GPL-2.0-only", 179 | "GPL-3.0-only" 180 | ], 181 | "authors": [ 182 | { 183 | "name": "David Grudl", 184 | "homepage": "https://davidgrudl.com" 185 | }, 186 | { 187 | "name": "Miloslav Hůla", 188 | "homepage": "https://github.com/milo" 189 | }, 190 | { 191 | "name": "Nette Community", 192 | "homepage": "https://nette.org/contributors" 193 | } 194 | ], 195 | "description": "Nette Tester: enjoyable unit testing in PHP with code coverage reporter. 🍏🍏🍎🍏", 196 | "homepage": "https://tester.nette.org", 197 | "keywords": [ 198 | "Xdebug", 199 | "assertions", 200 | "clover", 201 | "code coverage", 202 | "nette", 203 | "pcov", 204 | "phpdbg", 205 | "phpunit", 206 | "testing", 207 | "unit" 208 | ], 209 | "support": { 210 | "issues": "https://github.com/nette/tester/issues", 211 | "source": "https://github.com/nette/tester/tree/v2.5.4" 212 | }, 213 | "time": "2024-10-23T23:57:10+00:00" 214 | }, 215 | { 216 | "name": "phpstan/phpdoc-parser", 217 | "version": "2.1.0", 218 | "source": { 219 | "type": "git", 220 | "url": "https://github.com/phpstan/phpdoc-parser.git", 221 | "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" 222 | }, 223 | "dist": { 224 | "type": "zip", 225 | "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", 226 | "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", 227 | "shasum": "" 228 | }, 229 | "require": { 230 | "php": "^7.4 || ^8.0" 231 | }, 232 | "require-dev": { 233 | "doctrine/annotations": "^2.0", 234 | "nikic/php-parser": "^5.3.0", 235 | "php-parallel-lint/php-parallel-lint": "^1.2", 236 | "phpstan/extension-installer": "^1.0", 237 | "phpstan/phpstan": "^2.0", 238 | "phpstan/phpstan-phpunit": "^2.0", 239 | "phpstan/phpstan-strict-rules": "^2.0", 240 | "phpunit/phpunit": "^9.6", 241 | "symfony/process": "^5.2" 242 | }, 243 | "type": "library", 244 | "autoload": { 245 | "psr-4": { 246 | "PHPStan\\PhpDocParser\\": [ 247 | "src/" 248 | ] 249 | } 250 | }, 251 | "notification-url": "https://packagist.org/downloads/", 252 | "license": [ 253 | "MIT" 254 | ], 255 | "description": "PHPDoc parser with support for nullable, intersection and generic types", 256 | "support": { 257 | "issues": "https://github.com/phpstan/phpdoc-parser/issues", 258 | "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" 259 | }, 260 | "time": "2025-02-19T13:28:12+00:00" 261 | }, 262 | { 263 | "name": "phpstan/phpstan", 264 | "version": "1.12.7", 265 | "source": { 266 | "type": "git", 267 | "url": "https://github.com/phpstan/phpstan.git", 268 | "reference": "dc2b9976bd8b0f84ec9b0e50cc35378551de7af0" 269 | }, 270 | "dist": { 271 | "type": "zip", 272 | "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc2b9976bd8b0f84ec9b0e50cc35378551de7af0", 273 | "reference": "dc2b9976bd8b0f84ec9b0e50cc35378551de7af0", 274 | "shasum": "" 275 | }, 276 | "require": { 277 | "php": "^7.2|^8.0" 278 | }, 279 | "conflict": { 280 | "phpstan/phpstan-shim": "*" 281 | }, 282 | "bin": [ 283 | "phpstan", 284 | "phpstan.phar" 285 | ], 286 | "type": "library", 287 | "autoload": { 288 | "files": [ 289 | "bootstrap.php" 290 | ] 291 | }, 292 | "notification-url": "https://packagist.org/downloads/", 293 | "license": [ 294 | "MIT" 295 | ], 296 | "description": "PHPStan - PHP Static Analysis Tool", 297 | "keywords": [ 298 | "dev", 299 | "static analysis" 300 | ], 301 | "support": { 302 | "docs": "https://phpstan.org/user-guide/getting-started", 303 | "forum": "https://github.com/phpstan/phpstan/discussions", 304 | "issues": "https://github.com/phpstan/phpstan/issues", 305 | "security": "https://github.com/phpstan/phpstan/security/policy", 306 | "source": "https://github.com/phpstan/phpstan-src" 307 | }, 308 | "funding": [ 309 | { 310 | "url": "https://github.com/ondrejmirtes", 311 | "type": "github" 312 | }, 313 | { 314 | "url": "https://github.com/phpstan", 315 | "type": "github" 316 | } 317 | ], 318 | "time": "2024-10-18T11:12:07+00:00" 319 | }, 320 | { 321 | "name": "phpstan/phpstan-strict-rules", 322 | "version": "1.6.1", 323 | "source": { 324 | "type": "git", 325 | "url": "https://github.com/phpstan/phpstan-strict-rules.git", 326 | "reference": "daeec748b53de80a97498462513066834ec28f8b" 327 | }, 328 | "dist": { 329 | "type": "zip", 330 | "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/daeec748b53de80a97498462513066834ec28f8b", 331 | "reference": "daeec748b53de80a97498462513066834ec28f8b", 332 | "shasum": "" 333 | }, 334 | "require": { 335 | "php": "^7.2 || ^8.0", 336 | "phpstan/phpstan": "^1.12.4" 337 | }, 338 | "require-dev": { 339 | "nikic/php-parser": "^4.13.0", 340 | "php-parallel-lint/php-parallel-lint": "^1.2", 341 | "phpstan/phpstan-deprecation-rules": "^1.1", 342 | "phpstan/phpstan-phpunit": "^1.0", 343 | "phpunit/phpunit": "^9.5" 344 | }, 345 | "type": "phpstan-extension", 346 | "extra": { 347 | "phpstan": { 348 | "includes": [ 349 | "rules.neon" 350 | ] 351 | } 352 | }, 353 | "autoload": { 354 | "psr-4": { 355 | "PHPStan\\": "src/" 356 | } 357 | }, 358 | "notification-url": "https://packagist.org/downloads/", 359 | "license": [ 360 | "MIT" 361 | ], 362 | "description": "Extra strict and opinionated rules for PHPStan", 363 | "support": { 364 | "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", 365 | "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.1" 366 | }, 367 | "time": "2024-09-20T14:04:44+00:00" 368 | }, 369 | { 370 | "name": "slevomat/coding-standard", 371 | "version": "8.18.1", 372 | "source": { 373 | "type": "git", 374 | "url": "https://github.com/slevomat/coding-standard.git", 375 | "reference": "06b18b3f64979ab31d27c37021838439f3ed5919" 376 | }, 377 | "dist": { 378 | "type": "zip", 379 | "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/06b18b3f64979ab31d27c37021838439f3ed5919", 380 | "reference": "06b18b3f64979ab31d27c37021838439f3ed5919", 381 | "shasum": "" 382 | }, 383 | "require": { 384 | "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", 385 | "php": "^7.4 || ^8.0", 386 | "phpstan/phpdoc-parser": "^2.1.0", 387 | "squizlabs/php_codesniffer": "^3.13.0" 388 | }, 389 | "require-dev": { 390 | "phing/phing": "3.0.1", 391 | "php-parallel-lint/php-parallel-lint": "1.4.0", 392 | "phpstan/phpstan": "2.1.17", 393 | "phpstan/phpstan-deprecation-rules": "2.0.3", 394 | "phpstan/phpstan-phpunit": "2.0.6", 395 | "phpstan/phpstan-strict-rules": "2.0.4", 396 | "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.21|12.1.3" 397 | }, 398 | "type": "phpcodesniffer-standard", 399 | "extra": { 400 | "branch-alias": { 401 | "dev-master": "8.x-dev" 402 | } 403 | }, 404 | "autoload": { 405 | "psr-4": { 406 | "SlevomatCodingStandard\\": "SlevomatCodingStandard/" 407 | } 408 | }, 409 | "notification-url": "https://packagist.org/downloads/", 410 | "license": [ 411 | "MIT" 412 | ], 413 | "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", 414 | "keywords": [ 415 | "dev", 416 | "phpcs" 417 | ], 418 | "support": { 419 | "issues": "https://github.com/slevomat/coding-standard/issues", 420 | "source": "https://github.com/slevomat/coding-standard/tree/8.18.1" 421 | }, 422 | "funding": [ 423 | { 424 | "url": "https://github.com/kukulich", 425 | "type": "github" 426 | }, 427 | { 428 | "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", 429 | "type": "tidelift" 430 | } 431 | ], 432 | "time": "2025-05-22T14:32:30+00:00" 433 | }, 434 | { 435 | "name": "squizlabs/php_codesniffer", 436 | "version": "3.13.0", 437 | "source": { 438 | "type": "git", 439 | "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", 440 | "reference": "65ff2489553b83b4597e89c3b8b721487011d186" 441 | }, 442 | "dist": { 443 | "type": "zip", 444 | "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/65ff2489553b83b4597e89c3b8b721487011d186", 445 | "reference": "65ff2489553b83b4597e89c3b8b721487011d186", 446 | "shasum": "" 447 | }, 448 | "require": { 449 | "ext-simplexml": "*", 450 | "ext-tokenizer": "*", 451 | "ext-xmlwriter": "*", 452 | "php": ">=5.4.0" 453 | }, 454 | "require-dev": { 455 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" 456 | }, 457 | "bin": [ 458 | "bin/phpcbf", 459 | "bin/phpcs" 460 | ], 461 | "type": "library", 462 | "extra": { 463 | "branch-alias": { 464 | "dev-master": "3.x-dev" 465 | } 466 | }, 467 | "notification-url": "https://packagist.org/downloads/", 468 | "license": [ 469 | "BSD-3-Clause" 470 | ], 471 | "authors": [ 472 | { 473 | "name": "Greg Sherwood", 474 | "role": "Former lead" 475 | }, 476 | { 477 | "name": "Juliette Reinders Folmer", 478 | "role": "Current lead" 479 | }, 480 | { 481 | "name": "Contributors", 482 | "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" 483 | } 484 | ], 485 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 486 | "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", 487 | "keywords": [ 488 | "phpcs", 489 | "standards", 490 | "static analysis" 491 | ], 492 | "support": { 493 | "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", 494 | "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", 495 | "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", 496 | "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" 497 | }, 498 | "funding": [ 499 | { 500 | "url": "https://github.com/PHPCSStandards", 501 | "type": "github" 502 | }, 503 | { 504 | "url": "https://github.com/jrfnl", 505 | "type": "github" 506 | }, 507 | { 508 | "url": "https://opencollective.com/php_codesniffer", 509 | "type": "open_collective" 510 | }, 511 | { 512 | "url": "https://thanks.dev/u/gh/phpcsstandards", 513 | "type": "thanks_dev" 514 | } 515 | ], 516 | "time": "2025-05-11T03:36:00+00:00" 517 | } 518 | ], 519 | "aliases": [], 520 | "minimum-stability": "stable", 521 | "stability-flags": [], 522 | "prefer-stable": false, 523 | "prefer-lowest": false, 524 | "platform": { 525 | "php": ">=8.0", 526 | "ext-dom": "*", 527 | "ext-libxml": "*" 528 | }, 529 | "platform-dev": [], 530 | "plugin-api-version": "2.3.0" 531 | } 532 | -------------------------------------------------------------------------------- /licence.md: -------------------------------------------------------------------------------- 1 | ## License 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2016 Jan Nedbal 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | src/ 19 | tests/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | Exception type missing for @throws annotation 173 | 174 | 175 | Only 1 @return annotation is allowed in a function comment 176 | 177 | 178 | Extra @param annotation 179 | 180 | 181 | @param annotation for parameter "%s" missing 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 5 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 370 | 371 | 372 | 373 | 374 | 375 | 379 | 380 | 381 | 382 | 383 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - phar://phpstan.phar/conf/config.levelmax.neon 3 | - phar://phpstan.phar/conf/bleedingEdge.neon 4 | - ./vendor/phpstan/phpstan-strict-rules/rules.neon 5 | 6 | parameters: 7 | paths: 8 | - src 9 | - tests 10 | tmpDir: %rootDir%/../../../cache/phpstan/ 11 | checkMissingCallableSignature: true 12 | checkUninitializedProperties: true 13 | checkTooWideReturnTypesInProtectedAndPublicMethods: true 14 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | This library provides simple interface for loading XML or HTML strings to DomDocument object. 4 | It prevents some known vulnerabilities and allows you to handle LibXML errors simply by catching XmlException as you can see below. 5 | 6 | ## Installation 7 | 8 | ```sh 9 | $ composer require lightools/xml 10 | ``` 11 | 12 | ## Simple usage 13 | 14 | Both loading methods (loadXml and loadHtml) return DomDocument. 15 | If you prefer working with SimpleXmlElement, you can use [simplexml_import_dom](https://secure.php.net/manual/en/function.simplexml-import-dom.php) function. 16 | 17 | ```php 18 | $xml = 'text'; 19 | $html = 'Foo'; 20 | 21 | $loader = new Lightools\Xml\XmlLoader(); 22 | 23 | try { 24 | $xmlDomDocument = $loader->loadXml($xml); 25 | $htmlDomDocument = $loader->loadHtml($html); 26 | 27 | } catch (Lightools\Xml\XmlException $e) { 28 | // process exception 29 | } 30 | ``` 31 | 32 | ## How to run checks 33 | 34 | ```sh 35 | $ composer check 36 | ``` 37 | 38 | 39 | ## Versions 40 | 41 | - v1.x is for PHP 5.4 and higher 42 | - v2.x is for PHP 7.1 and higher 43 | - v3.x is for PHP 8.0 and higher 44 | -------------------------------------------------------------------------------- /src/Xml/XmlException.php: -------------------------------------------------------------------------------- 1 | error = $error; 20 | $info = trim($error->message) . " on line $error->line and column $error->column"; 21 | 22 | $errorMessage = match ($error->level) { 23 | LIBXML_ERR_WARNING => "XML Warning #$error->code: $info", 24 | LIBXML_ERR_ERROR => "XML Error #$error->code: $info", 25 | LIBXML_ERR_FATAL => "XML Fatal Error #$error->code: $info", 26 | default => "Unknown XML failure #$error->code: $info", 27 | }; 28 | 29 | parent::__construct($errorMessage, $error->code); 30 | } 31 | 32 | public function getError(): LibXMLError 33 | { 34 | return $this->error; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/Xml/XmlLoader.php: -------------------------------------------------------------------------------- 1 | load($xml, self::LOAD_XML); 27 | $this->checkDomDocumentChildren($domDocument); 28 | return $domDocument; 29 | } 30 | 31 | /** 32 | * @throws XmlException When parsing fails 33 | */ 34 | public function loadHtml(string $html): DOMDocument 35 | { 36 | return $this->load($html, self::LOAD_HTML); 37 | } 38 | 39 | /** 40 | * @throws XmlException 41 | */ 42 | private function load(string $source, string $method): DOMDocument 43 | { 44 | if ($source === '') { 45 | throw new XmlException($this->getCustomError('Empty string supplied as input')); 46 | } 47 | 48 | $internalErrorsOld = libxml_use_internal_errors(true); 49 | 50 | $dom = new DOMDocument(); 51 | 52 | if ($method === self::LOAD_XML) { 53 | $success = $dom->loadXML($source, LIBXML_NONET | LIBXML_NOBLANKS); 54 | } else { 55 | $success = $dom->loadHTML($source, LIBXML_NONET | LIBXML_NOBLANKS); 56 | } 57 | 58 | $error = libxml_get_last_error(); 59 | 60 | libxml_clear_errors(); 61 | libxml_use_internal_errors($internalErrorsOld); 62 | 63 | if ($success === false) { 64 | throw new XmlException($error !== false ? $error : $this->getCustomError('Unknown error')); 65 | } 66 | 67 | return $dom; 68 | } 69 | 70 | /** 71 | * @see http://stackoverflow.com/a/10218526/1542616 72 | * @throws XmlException 73 | */ 74 | private function checkDomDocumentChildren(DOMDocument $dom): void 75 | { 76 | foreach ($dom->childNodes as $child) { 77 | if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) { 78 | throw new XmlException($this->getCustomError('Document types are not allowed')); 79 | } 80 | } 81 | } 82 | 83 | private function getCustomError(string $message): LibXMLError 84 | { 85 | $err = new LibXMLError(); 86 | $err->level = LIBXML_ERR_FATAL; 87 | $err->message = $message; 88 | $err->code = 0; 89 | $err->column = 0; 90 | $err->line = 0; 91 | $err->file = ''; 92 | return $err; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /tests/XmlLoaderTest.phpt: -------------------------------------------------------------------------------- 1 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ]> 37 | &lol9; 38 | '); 39 | 40 | if (LIBXML_VERSION >= self::LIBXML_WITH_ENTITY_EXPANSION_PROTECTION) { 41 | $error = 'XML Fatal Error #89: Maximum entity amplification factor exceeded on line 1 and column 25'; 42 | } else { 43 | $error = 'XML Fatal Error #89: Detected an entity reference loop on line 14 and column 21'; 44 | } 45 | 46 | Assert::exception(function () use ($source): void { 47 | $loader = new XmlLoader(); 48 | $loader->loadXml($source); 49 | }, XmlException::class, $error); 50 | } 51 | 52 | public function testQuadraticBlowup(): void { 53 | $source = trim(' 54 | 55 | 57 | ]> 58 | ' . str_repeat('&a;', 100000) . ' 59 | '); 60 | 61 | if (LIBXML_VERSION >= self::LIBXML_WITH_ENTITY_EXPANSION_PROTECTION) { 62 | $error = 'XML Fatal Error #89: Maximum entity amplification factor exceeded on line 5 and column 47'; 63 | } else { 64 | $error = 'XML Fatal Error #0: Document types are not allowed on line 0 and column 0'; 65 | } 66 | 67 | Assert::exception(function () use ($source): void { 68 | $loader = new XmlLoader(); 69 | (string) $loader->loadXml($source); 70 | }, XmlException::class, $error); 71 | } 72 | 73 | public function testEmptySource(): void { 74 | Assert::exception(function () { 75 | $loader = new XmlLoader(); 76 | $loader->loadXml(''); 77 | }, XmlException::class, 'XML Fatal Error #0: Empty string supplied as input on line 0 and column 0'); 78 | } 79 | 80 | public function testInvalidXml(): void { 81 | $source = trim(' 82 | 83 | 84 | '); 85 | 86 | if (LIBXML_VERSION < 20911) { 87 | $error = 'XML Fatal Error #74: EndTag: \'loadXml($source); 95 | }, XmlException::class, $error); 96 | } 97 | 98 | public function testValidXml(): void { 99 | $source = trim(' 100 | 101 | 102 | John 103 | Jack 104 | Reminder 105 | Don\'t forget me this weekend! 106 | 107 | '); 108 | 109 | $loader = new XmlLoader(); 110 | $xml = $loader->loadXml($source); 111 | Assert::same('Jack', $xml->getElementsByTagName('from')->item(0)->nodeValue); 112 | } 113 | 114 | public function testValidHtml(): void { 115 | $source = trim(' 116 | 117 | 118 | 119 | 120 | Foo 121 | 122 | 123 |

I\'m the content

124 | 125 | 126 | '); 127 | 128 | $loader = new XmlLoader(); 129 | $xml = $loader->loadHtml($source); 130 | Assert::same('Foo', $xml->getElementsByTagName('title')->item(0)->nodeValue); 131 | } 132 | 133 | } 134 | 135 | (new XmlLoaderTest)->run(); 136 | --------------------------------------------------------------------------------