feat(demo): add story 1 — Sorano: Rock and Time
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
name: Autocloser
|
||||
on: [issues, pull_request]
|
||||
jobs:
|
||||
autoclose:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Autoclose new issues and PRs
|
||||
uses: roots/issue-closer@v1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
|
||||
issue-pattern: "^exact-string-will-never-match$"
|
||||
pr-pattern: "^exact-string-will-never-match$"
|
||||
issue-close-message: |
|
||||
This package is considered feature-complete, and is now in **security-only** maintenance mode, following a [decision by the Technical Steering Committee](https://github.com/laminas/technical-steering-committee/blob/2b55453e172a1b8c9c4c212be7cf7e7a58b9352c/meetings/minutes/2020-08-03-TSC-Minutes.md#vote-on-components-to-mark-as-security-only).
|
||||
If you have a security issue, please [follow our security reporting guidelines](https://getlaminas.org/security/).
|
||||
If you wish to take on the role of maintainer, please [nominate yourself](https://github.com/laminas/technical-steering-committee/issues/new?assignees=&labels=Nomination&template=Maintainer_Nomination.md&title=%5BNOMINATION%5D%5BMAINTAINER%5D%3A+%7Bname+of+person+being+nominated%7D)
|
||||
|
||||
pr-close-message: |
|
||||
This package is considered feature-complete, and is now in **security-only** maintenance mode, following a [decision by the Technical Steering Committee](https://github.com/laminas/technical-steering-committee/blob/2b55453e172a1b8c9c4c212be7cf7e7a58b9352c/meetings/minutes/2020-08-03-TSC-Minutes.md#vote-on-components-to-mark-as-security-only).
|
||||
If you have a security issue, please [follow our security reporting guidelines](https://getlaminas.org/security/).
|
||||
If you wish to take on the role of maintainer, please [nominate yourself](https://github.com/laminas/technical-steering-committee/issues/new?assignees=&labels=Nomination&template=Maintainer_Nomination.md&title=%5BNOMINATION%5D%5BMAINTAINER%5D%3A+%7Bname+of+person+being+nominated%7D)
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
name: "Continuous Integration"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- '[0-9]+.[0-9]+.x'
|
||||
- 'refs/pull/*'
|
||||
tags:
|
||||
|
||||
jobs:
|
||||
matrix:
|
||||
name: Generate job matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Gather CI configuration
|
||||
id: matrix
|
||||
uses: laminas/laminas-ci-matrix-action@v1
|
||||
|
||||
qa:
|
||||
name: QA Checks
|
||||
needs: [matrix]
|
||||
runs-on: ${{ matrix.operatingSystem }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }}
|
||||
steps:
|
||||
- name: ${{ matrix.name }}
|
||||
uses: laminas/laminas-continuous-integration-action@v1
|
||||
with:
|
||||
job: ${{ matrix.job }}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
# Alternate workflow example.
|
||||
# This one is identical to the one in release-on-milestone.yml, with one change:
|
||||
# the Release step uses the ORGANIZATION_ADMIN_TOKEN instead, to allow it to
|
||||
# trigger a release workflow event. This is useful if you have other actions
|
||||
# that intercept that event.
|
||||
|
||||
name: "Automatic Releases"
|
||||
|
||||
on:
|
||||
milestone:
|
||||
types:
|
||||
- "closed"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: "GIT tag, release & create merge-up PR"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v2"
|
||||
|
||||
- name: "Release"
|
||||
uses: "laminas/automatic-releases@v1"
|
||||
with:
|
||||
command-name: "laminas:automatic-releases:release"
|
||||
env:
|
||||
"GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
|
||||
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
|
||||
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
|
||||
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||
|
||||
- name: "Create Merge-Up Pull Request"
|
||||
uses: "laminas/automatic-releases@v1"
|
||||
with:
|
||||
command-name: "laminas:automatic-releases:create-merge-up-pull-request"
|
||||
env:
|
||||
"GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
|
||||
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
|
||||
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
|
||||
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||
|
||||
- name: "Create and/or Switch to new Release Branch"
|
||||
uses: "laminas/automatic-releases@v1"
|
||||
with:
|
||||
command-name: "laminas:automatic-releases:switch-default-branch-to-next-minor"
|
||||
env:
|
||||
"GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
|
||||
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
|
||||
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
|
||||
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||
|
||||
- name: "Bump Changelog Version On Originating Release Branch"
|
||||
uses: "laminas/automatic-releases@v1"
|
||||
with:
|
||||
command-name: "laminas:automatic-releases:bump-changelog"
|
||||
env:
|
||||
"GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
|
||||
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
|
||||
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
|
||||
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||
|
||||
- name: "Create new milestones"
|
||||
uses: "laminas/automatic-releases@v1"
|
||||
with:
|
||||
command-name: "laminas:automatic-releases:create-milestones"
|
||||
env:
|
||||
"GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
|
||||
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
|
||||
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
|
||||
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||
@@ -0,0 +1 @@
|
||||
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/)
|
||||
@@ -0,0 +1,26 @@
|
||||
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
- Neither the name of Laminas Foundation nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,66 @@
|
||||
# laminas-xml
|
||||
|
||||
> ## 🇷🇺 Русским гражданам
|
||||
>
|
||||
> Мы, участники Laminas, родились и живем в разных странах. У многих из нас есть друзья, родственники и коллеги как в России, так и в Украине. Некоторые из нас родились в России. Некоторые из нас живут в России. У некоторых бабушки и дедушки сражались с фашистами во Второй мировой войне. Здесь никто не поддерживает фашизм.
|
||||
>
|
||||
> У одного из нас есть украинская родственница, которая спаслась из дома вместе с сыном. Поезд задержался из-за бомбежки на дороге впереди. У нас есть друзья, которые прячутся в бомбоубежищах. Мы с тревогой ждем весточки от них после воздушных налетов, которые беспорядочно наносят удары и попадают по больницам, школам, детским садам и домам. Мы не берем это из каких-либо СМИ. Мы наблюдаем это напрямую.
|
||||
>
|
||||
> Вы доверяете нам достаточно, чтоб использовать наши программы, и мы просим вас довериться нам вновь. Мы нуждаемся в помощи. Выходите и протестуйте против этой бесполезной войны. Остановите кровопролитие. Скажите "Нет войне!"
|
||||
>
|
||||
> ## 🇺🇸 To Citizens of Russia
|
||||
>
|
||||
> We at Laminas come from all over the world. Many of us have friends, family and colleagues in both Russia and Ukraine. Some of us were born in Russia. Some of us currently live in Russia. Some have grandparents who fought Nazis in World War II. Nobody here supports fascism.
|
||||
>
|
||||
> One team member has a Ukrainian relative who fled her home with her son. The train was delayed due to bombing on the road ahead. We have friends who are hiding in bomb shelters. We anxiously follow up on them after the air raids, which indiscriminately fire at hospitals, schools, kindergartens and houses. We're not taking this from any media. These are our actual experiences.
|
||||
>
|
||||
> You trust us enough to use our software. We ask that you trust us to say the truth on this. We need your help. Go out and protest this unnecessary war. Stop the bloodshed. Say "stop the war!"
|
||||
|
||||
> This package is considered feature-complete, and is now in **security-only** maintenance mode, following a [decision by the Technical Steering Committee](https://github.com/laminas/technical-steering-committee/blob/2b55453e172a1b8c9c4c212be7cf7e7a58b9352c/meetings/minutes/2020-08-03-TSC-Minutes.md#vote-on-components-to-mark-as-security-only).
|
||||
> If you have a security issue, please [follow our security reporting guidelines](https://getlaminas.org/security/).
|
||||
> If you wish to take on the role of maintainer, please [nominate yourself](https://github.com/laminas/technical-steering-committee/issues/new?assignees=&labels=Nomination&template=Maintainer_Nomination.md&title=%5BNOMINATION%5D%5BMAINTAINER%5D%3A+%7Bname+of+person+being+nominated%7D)
|
||||
|
||||
[](https://github.com/laminas/laminas-xml/actions?query=workflow%3A"Continuous+Integration")
|
||||
|
||||
An utility component for XML usage and best practices in PHP
|
||||
|
||||
## Installation
|
||||
|
||||
You can install using:
|
||||
|
||||
```bash
|
||||
$ curl -s https://getcomposer.org/installer | php
|
||||
$ php composer.phar install
|
||||
```
|
||||
|
||||
Notice that this library doesn't have any external dependencies, the usage of composer is for autoloading and standard purpose.
|
||||
|
||||
## Laminas\Xml\Security
|
||||
|
||||
This is a security component to prevent [XML eXternal Entity](https://www.owasp.org/index.php/XML_External_Entity_%28XXE%29_Processing) (XXE) and [XML Entity Expansion](http://projects.webappsec.org/w/page/13247002/XML%20Entity%20Expansion) (XEE) attacks on XML documents.
|
||||
|
||||
The XXE attack is prevented disabling the load of external entities in the libxml library used by PHP, using the function [libxml_disable_entity_loader](http://www.php.net/manual/en/function.libxml-disable-entity-loader.php).
|
||||
|
||||
The XEE attack is prevented looking inside the XML document for ENTITY usage. If the XML document uses ENTITY the library throw an Exception.
|
||||
|
||||
We have two static methods to scan and load XML document from a string (scan) and from a file (scanFile). You can decide to get a SimpleXMLElement or DOMDocument as result, using the following use cases:
|
||||
|
||||
```php
|
||||
use Laminas\Xml\Security as XmlSecurity;
|
||||
|
||||
$xml = <<<XML
|
||||
<?xml version="1.0"?>
|
||||
<results>
|
||||
<result>test</result>
|
||||
</results>
|
||||
XML;
|
||||
|
||||
// SimpleXML use case
|
||||
$simplexml = XmlSecurity::scan($xml);
|
||||
printf ("SimpleXMLElement: %s\n", ($simplexml instanceof \SimpleXMLElement) ? 'yes' : 'no');
|
||||
|
||||
// DOMDocument use case
|
||||
$dom = new \DOMDocument('1.0');
|
||||
$dom = XmlSecurity::scan($xml, $dom);
|
||||
printf ("DOMDocument: %s\n", ($dom instanceof \DOMDocument) ? 'yes' : 'no');
|
||||
```
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "laminas/laminas-xml",
|
||||
"description": "Utility library for XML usage, best practices, and security in PHP",
|
||||
"license": "BSD-3-Clause",
|
||||
"keywords": [
|
||||
"laminas",
|
||||
"xml",
|
||||
"security"
|
||||
],
|
||||
"homepage": "https://laminas.dev",
|
||||
"support": {
|
||||
"issues": "https://github.com/laminas/laminas-xml/issues",
|
||||
"source": "https://github.com/laminas/laminas-xml",
|
||||
"rss": "https://github.com/laminas/laminas-xml/releases.atom",
|
||||
"chat": "https://laminas.dev/chat",
|
||||
"forum": "https://discourse.laminas.dev"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
"platform": {
|
||||
"php": "8.1.99"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
|
||||
"ext-dom": "*",
|
||||
"ext-simplexml": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-iconv": "*",
|
||||
"laminas/laminas-coding-standard": "~1.0.0",
|
||||
"phpunit/phpunit": "^10.5.35 || ^11.4",
|
||||
"squizlabs/php_codesniffer": "3.10.3 as 2.9999999.9999999"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laminas\\Xml\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"LaminasTest\\Xml\\": "test/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"check": [
|
||||
"@cs-check",
|
||||
"@test"
|
||||
],
|
||||
"cs-check": "phpcs",
|
||||
"cs-fix": "phpcbf",
|
||||
"test": "phpunit --colors=always",
|
||||
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
|
||||
},
|
||||
"conflict": {
|
||||
"zendframework/zendxml": "*"
|
||||
}
|
||||
}
|
||||
+1844
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"local>laminas/.github:renovate-config-security-updates-only"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Laminas\Xml\Exception;
|
||||
|
||||
interface ExceptionInterface
|
||||
{
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Laminas\Xml\Exception;
|
||||
|
||||
/**
|
||||
* Invalid argument exception
|
||||
*/
|
||||
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Laminas\Xml\Exception;
|
||||
|
||||
/**
|
||||
* Runtime exception
|
||||
*/
|
||||
class RuntimeException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
<?php
|
||||
|
||||
namespace Laminas\Xml;
|
||||
|
||||
use DOMDocument;
|
||||
use SimpleXMLElement;
|
||||
|
||||
class Security
|
||||
{
|
||||
const ENTITY_DETECT = 'Detected use of ENTITY in XML, disabled to prevent XXE/XEE attacks';
|
||||
|
||||
/**
|
||||
* Heuristic scan to detect entity in XML
|
||||
*
|
||||
* @param string $xml
|
||||
* @throws Exception\RuntimeException If entity expansion or external entity declaration was discovered.
|
||||
*/
|
||||
protected static function heuristicScan($xml)
|
||||
{
|
||||
foreach (self::getEntityComparison($xml) as $compare) {
|
||||
if (strpos($xml, $compare) !== false) {
|
||||
throw new Exception\RuntimeException(self::ENTITY_DETECT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan XML string for potential XXE and XEE attacks
|
||||
*
|
||||
* @param string $xml
|
||||
* @param int $libXmlConstants additional libxml constants to pass in
|
||||
* @param callable $callback the callback to use to create the dom element
|
||||
* @param DomDocument|null $dom
|
||||
* @return SimpleXMLElement|DomDocument|boolean
|
||||
* @throws Exception\RuntimeException
|
||||
*/
|
||||
private static function scanString($xml, $libXmlConstants, callable $callback, DOMDocument|null $dom = null)
|
||||
{
|
||||
// If running with PHP-FPM we perform an heuristic scan
|
||||
// We cannot use libxml_disable_entity_loader because of this bug
|
||||
// @see https://bugs.php.net/bug.php?id=64938
|
||||
if (self::isPhpFpm()) {
|
||||
self::heuristicScan($xml);
|
||||
}
|
||||
|
||||
if (null === $dom) {
|
||||
$simpleXml = true;
|
||||
$dom = new DOMDocument();
|
||||
}
|
||||
|
||||
if (! self::isPhpFpm()) {
|
||||
if (\PHP_VERSION_ID < 80000) {
|
||||
$loadEntities = libxml_disable_entity_loader(true);
|
||||
}
|
||||
$useInternalXmlErrors = libxml_use_internal_errors(true);
|
||||
}
|
||||
|
||||
// Load XML with network access disabled (LIBXML_NONET)
|
||||
// error disabled with @ for PHP-FPM scenario
|
||||
set_error_handler(function ($errno, $errstr) {
|
||||
if (substr_count($errstr, 'DOMDocument::loadXML()') > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, E_WARNING);
|
||||
|
||||
$result = $callback($xml, $dom, LIBXML_NONET | $libXmlConstants);
|
||||
|
||||
restore_error_handler();
|
||||
|
||||
if (! $result) {
|
||||
// Entity load to previous setting
|
||||
if (! self::isPhpFpm()) {
|
||||
if (\PHP_VERSION_ID < 80000) {
|
||||
libxml_disable_entity_loader($loadEntities);
|
||||
}
|
||||
libxml_use_internal_errors($useInternalXmlErrors);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Scan for potential XEE attacks using ENTITY, if not PHP-FPM
|
||||
if (! self::isPhpFpm()) {
|
||||
foreach ($dom->childNodes as $child) {
|
||||
if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
|
||||
if ($child->entities->length > 0) {
|
||||
throw new Exception\RuntimeException(self::ENTITY_DETECT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Entity load to previous setting
|
||||
if (! self::isPhpFpm()) {
|
||||
if (\PHP_VERSION_ID < 80000) {
|
||||
libxml_disable_entity_loader($loadEntities);
|
||||
}
|
||||
libxml_use_internal_errors($useInternalXmlErrors);
|
||||
}
|
||||
|
||||
if (isset($simpleXml)) {
|
||||
$result = simplexml_import_dom($dom);
|
||||
if (! $result instanceof SimpleXMLElement) {
|
||||
return false;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
return $dom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan XML string for potential XXE and XEE attacks
|
||||
*
|
||||
* @param string $xml
|
||||
* @param DomDocument|null $dom
|
||||
* @param int $libXmlConstants additional libxml constants to pass in
|
||||
* @throws Exception\RuntimeException
|
||||
* @return SimpleXMLElement|DomDocument|boolean
|
||||
*/
|
||||
public static function scan($xml, DOMDocument|null $dom = null, $libXmlConstants = 0)
|
||||
{
|
||||
$callback = function ($xml, $dom, $constants) {
|
||||
return $dom->loadXml($xml, $constants);
|
||||
};
|
||||
return self::scanString($xml, $libXmlConstants, $callback, $dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan HTML string for potential XXE and XEE attacks
|
||||
*
|
||||
* @param string $xml
|
||||
* @param DomDocument|null $dom
|
||||
* @param int $libXmlConstants additional libxml constants to pass in
|
||||
* @throws Exception\RuntimeException
|
||||
* @return SimpleXMLElement|DomDocument|boolean
|
||||
*/
|
||||
public static function scanHtml($html, DOMDocument|null $dom = null, $libXmlConstants = 0)
|
||||
{
|
||||
$callback = function ($html, $dom, $constants) {
|
||||
return $dom->loadHtml($html, $constants);
|
||||
};
|
||||
return self::scanString($html, $libXmlConstants, $callback, $dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan XML file for potential XXE/XEE attacks
|
||||
*
|
||||
* @param string $file
|
||||
* @param DOMDocument|null $dom
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return SimpleXMLElement|DomDocument
|
||||
*/
|
||||
public static function scanFile($file, DOMDocument|null $dom = null)
|
||||
{
|
||||
if (! file_exists($file)) {
|
||||
throw new Exception\InvalidArgumentException(
|
||||
"The file $file specified doesn't exist"
|
||||
);
|
||||
}
|
||||
return self::scan(file_get_contents($file), $dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if PHP is running with PHP-FPM
|
||||
*
|
||||
* This method is mainly used to determine whether or not heuristic checks
|
||||
* (vs libxml checks) should be made, due to threading issues in libxml;
|
||||
* under php-fpm, threading becomes a concern.
|
||||
*
|
||||
* However, PHP versions 5.6.6+ contain a patch to the
|
||||
* libxml support in PHP that makes the libxml checks viable; in such
|
||||
* versions, this method will return false to enforce those checks, which
|
||||
* are more strict and accurate than the heuristic checks.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isPhpFpm()
|
||||
{
|
||||
$isVulnerableVersion = version_compare(PHP_VERSION, '5.6', 'ge')
|
||||
&& version_compare(PHP_VERSION, '5.6.6', 'lt');
|
||||
|
||||
if (0 === strpos(php_sapi_name(), 'fpm') && $isVulnerableVersion) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine and return the string(s) to use for the <!ENTITY comparison.
|
||||
*
|
||||
* @param string $xml
|
||||
* @return string[]
|
||||
*/
|
||||
protected static function getEntityComparison($xml)
|
||||
{
|
||||
$encodingMap = self::getAsciiEncodingMap();
|
||||
return array_map(function ($encoding) use ($encodingMap) {
|
||||
$generator = isset($encodingMap[$encoding]) ? $encodingMap[$encoding] : $encodingMap['UTF-8'];
|
||||
return $generator('<!ENTITY');
|
||||
}, self::detectXmlEncoding($xml, self::detectStringEncoding($xml)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the string encoding.
|
||||
*
|
||||
* Determines string encoding from either a detected BOM or a
|
||||
* heuristic.
|
||||
*
|
||||
* @param string $xml
|
||||
* @return string File encoding
|
||||
*/
|
||||
protected static function detectStringEncoding($xml)
|
||||
{
|
||||
return self::detectBom($xml) ?: self::detectXmlStringEncoding($xml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to match a known BOM.
|
||||
*
|
||||
* Iterates through the return of getBomMap(), comparing the initial bytes
|
||||
* of the provided string to the BOM of each; if a match is determined,
|
||||
* it returns the encoding.
|
||||
*
|
||||
* @param string $string
|
||||
* @return false|string Returns encoding on success.
|
||||
*/
|
||||
protected static function detectBom($string)
|
||||
{
|
||||
foreach (self::getBomMap() as $criteria) {
|
||||
if (0 === strncmp($string, $criteria['bom'], $criteria['length'])) {
|
||||
return $criteria['encoding'];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to detect the string encoding of an XML string.
|
||||
*
|
||||
* @param string $xml
|
||||
* @return string Encoding
|
||||
*/
|
||||
protected static function detectXmlStringEncoding($xml)
|
||||
{
|
||||
foreach (self::getAsciiEncodingMap() as $encoding => $generator) {
|
||||
$prefix = $generator('<' . '?xml');
|
||||
if (0 === strncmp($xml, $prefix, strlen($prefix))) {
|
||||
return $encoding;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return 'UTF-8';
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to detect the specified XML encoding.
|
||||
*
|
||||
* Using the file's encoding, determines if an "encoding" attribute is
|
||||
* present and well-formed in the XML declaration; if so, it returns a
|
||||
* list with both the ASCII representation of that declaration and the
|
||||
* original file encoding.
|
||||
*
|
||||
* If not, a list containing only the provided file encoding is returned.
|
||||
*
|
||||
* @param string $xml
|
||||
* @param string $fileEncoding
|
||||
* @return string[] Potential XML encodings
|
||||
*/
|
||||
protected static function detectXmlEncoding($xml, $fileEncoding)
|
||||
{
|
||||
$encodingMap = self::getAsciiEncodingMap();
|
||||
$generator = $encodingMap[$fileEncoding];
|
||||
$encAttr = $generator('encoding="');
|
||||
$quote = $generator('"');
|
||||
$close = $generator('>');
|
||||
|
||||
$closePos = strpos($xml, $close);
|
||||
if (false === $closePos) {
|
||||
return [$fileEncoding];
|
||||
}
|
||||
|
||||
$encPos = strpos($xml, $encAttr);
|
||||
if (false === $encPos
|
||||
|| $encPos > $closePos
|
||||
) {
|
||||
return [$fileEncoding];
|
||||
}
|
||||
|
||||
$encPos += strlen($encAttr);
|
||||
$quotePos = strpos($xml, $quote, $encPos);
|
||||
if (false === $quotePos) {
|
||||
return [$fileEncoding];
|
||||
}
|
||||
|
||||
$encoding = self::substr($xml, $encPos, $quotePos);
|
||||
return [
|
||||
// Following line works because we're only supporting 8-bit safe encodings at this time.
|
||||
str_replace('\0', '', $encoding), // detected encoding
|
||||
$fileEncoding, // file encoding
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of BOM maps.
|
||||
*
|
||||
* Returns a list of common encoding -> BOM maps, along with the character
|
||||
* length to compare against.
|
||||
*
|
||||
* @link https://en.wikipedia.org/wiki/Byte_order_mark
|
||||
* @return array
|
||||
*/
|
||||
protected static function getBomMap()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'encoding' => 'UTF-32BE',
|
||||
'bom' => pack('CCCC', 0x00, 0x00, 0xfe, 0xff),
|
||||
'length' => 4,
|
||||
],
|
||||
[
|
||||
'encoding' => 'UTF-32LE',
|
||||
'bom' => pack('CCCC', 0xff, 0xfe, 0x00, 0x00),
|
||||
'length' => 4,
|
||||
],
|
||||
[
|
||||
'encoding' => 'GB-18030',
|
||||
'bom' => pack('CCCC', 0x84, 0x31, 0x95, 0x33),
|
||||
'length' => 4,
|
||||
],
|
||||
[
|
||||
'encoding' => 'UTF-16BE',
|
||||
'bom' => pack('CC', 0xfe, 0xff),
|
||||
'length' => 2,
|
||||
],
|
||||
[
|
||||
'encoding' => 'UTF-16LE',
|
||||
'bom' => pack('CC', 0xff, 0xfe),
|
||||
'length' => 2,
|
||||
],
|
||||
[
|
||||
'encoding' => 'UTF-8',
|
||||
'bom' => pack('CCC', 0xef, 0xbb, 0xbf),
|
||||
'length' => 3,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a map of encoding => generator pairs.
|
||||
*
|
||||
* Returns a map of encoding => generator pairs, where the generator is a
|
||||
* callable that accepts a string and returns the appropriate byte order
|
||||
* sequence of that string for the encoding.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function getAsciiEncodingMap()
|
||||
{
|
||||
return [
|
||||
'UTF-32BE' => function ($ascii) {
|
||||
return preg_replace('/(.)/', "\0\0\0\\1", $ascii);
|
||||
},
|
||||
'UTF-32LE' => function ($ascii) {
|
||||
return preg_replace('/(.)/', "\\1\0\0\0", $ascii);
|
||||
},
|
||||
'UTF-32odd1' => function ($ascii) {
|
||||
return preg_replace('/(.)/', "\0\\1\0\0", $ascii);
|
||||
},
|
||||
'UTF-32odd2' => function ($ascii) {
|
||||
return preg_replace('/(.)/', "\0\0\\1\0", $ascii);
|
||||
},
|
||||
'UTF-16BE' => function ($ascii) {
|
||||
return preg_replace('/(.)/', "\0\\1", $ascii);
|
||||
},
|
||||
'UTF-16LE' => function ($ascii) {
|
||||
return preg_replace('/(.)/', "\\1\0", $ascii);
|
||||
},
|
||||
'UTF-8' => function ($ascii) {
|
||||
return $ascii;
|
||||
},
|
||||
'GB-18030' => function ($ascii) {
|
||||
return $ascii;
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Binary-safe substr.
|
||||
*
|
||||
* substr() is not binary-safe; this method loops by character to ensure
|
||||
* multi-byte characters are aggregated correctly.
|
||||
*
|
||||
* @param string $string
|
||||
* @param int $start
|
||||
* @param int $end
|
||||
* @return string
|
||||
*/
|
||||
protected static function substr($string, $start, $end)
|
||||
{
|
||||
$substr = '';
|
||||
for ($i = $start; $i < $end; $i += 1) {
|
||||
$substr .= $string[$i];
|
||||
}
|
||||
return $substr;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user