mirror of
https://github.com/Combodo/iTop.git
synced 2026-05-19 23:32:17 +02:00
N°7920 - laminas-mail is an abandoned package, replace it with symfony/mailer (#742)
* N°7920 - laminas-mail is an abandoned package, replace it with symfony/mailer * Fix composer following merge
This commit is contained in:
200
lib/pelago/emogrifier/src/HtmlProcessor/CssVariableEvaluator.php
Normal file
200
lib/pelago/emogrifier/src/HtmlProcessor/CssVariableEvaluator.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pelago\Emogrifier\HtmlProcessor;
|
||||
|
||||
use Pelago\Emogrifier\Utilities\DeclarationBlockParser;
|
||||
use Pelago\Emogrifier\Utilities\Preg;
|
||||
|
||||
/**
|
||||
* This class can evaluate CSS custom properties that are defined and used in inline style attributes.
|
||||
*/
|
||||
final class CssVariableEvaluator extends AbstractHtmlProcessor
|
||||
{
|
||||
/**
|
||||
* temporary collection used by {@see replaceVariablesInDeclarations} and callee methods
|
||||
*
|
||||
* @var array<non-empty-string, string>
|
||||
*/
|
||||
private $currentVariableDefinitions = [];
|
||||
|
||||
/**
|
||||
* Replaces all CSS custom property references in inline style attributes with their corresponding values where
|
||||
* defined in inline style attributes (either from the element itself or the nearest ancestor).
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
public function evaluateVariables(): self
|
||||
{
|
||||
return $this->evaluateVariablesInElementAndDescendants($this->getHtmlElement(), []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<non-empty-string, string> $declarations
|
||||
*
|
||||
* @return array<non-empty-string, string>
|
||||
*/
|
||||
private function getVariableDefinitionsFromDeclarations(array $declarations): array
|
||||
{
|
||||
return \array_filter(
|
||||
$declarations,
|
||||
static function (string $key): bool {
|
||||
return \substr($key, 0, 2) === '--';
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function for {@see replaceVariablesInPropertyValue} performing regular expression replacement.
|
||||
*
|
||||
* @param array<int, string> $matches
|
||||
*/
|
||||
private function getPropertyValueReplacement(array $matches): string
|
||||
{
|
||||
$variableName = $matches[1];
|
||||
|
||||
if (isset($this->currentVariableDefinitions[$variableName])) {
|
||||
$variableValue = $this->currentVariableDefinitions[$variableName];
|
||||
} else {
|
||||
$fallbackValueSeparator = $matches[2] ?? '';
|
||||
if ($fallbackValueSeparator !== '') {
|
||||
$fallbackValue = $matches[3];
|
||||
// The fallback value may use other CSS variables, so recurse
|
||||
$variableValue = $this->replaceVariablesInPropertyValue($fallbackValue);
|
||||
} else {
|
||||
$variableValue = $matches[0];
|
||||
}
|
||||
}
|
||||
|
||||
return $variableValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regular expression based on {@see https://stackoverflow.com/a/54143883/2511031 a StackOverflow answer}.
|
||||
*/
|
||||
private function replaceVariablesInPropertyValue(string $propertyValue): string
|
||||
{
|
||||
return (new Preg())->replaceCallback(
|
||||
'/
|
||||
var\\(
|
||||
\\s*+
|
||||
# capture variable name including `--` prefix
|
||||
(
|
||||
--[^\\s\\),]++
|
||||
)
|
||||
\\s*+
|
||||
# capture optional fallback value
|
||||
(?:
|
||||
# capture separator to confirm there is a fallback value
|
||||
(,)\\s*
|
||||
# begin capture with named group that can be used recursively
|
||||
(?<recursable>
|
||||
# begin named group to match sequence without parentheses, except in strings
|
||||
(?<noparentheses>
|
||||
# repeated zero or more times:
|
||||
(?:
|
||||
# sequence without parentheses or quotes
|
||||
[^\\(\\)\'"]++
|
||||
|
|
||||
# string in double quotes
|
||||
"(?>[^"\\\\]++|\\\\.)*"
|
||||
|
|
||||
# string in single quotes
|
||||
\'(?>[^\'\\\\]++|\\\\.)*\'
|
||||
)*+
|
||||
)
|
||||
# repeated zero or more times:
|
||||
(?:
|
||||
# sequence in parentheses
|
||||
\\(
|
||||
# using the named recursable pattern
|
||||
(?&recursable)
|
||||
\\)
|
||||
# sequence without parentheses, except in strings
|
||||
(?&noparentheses)
|
||||
)*+
|
||||
)
|
||||
)?+
|
||||
\\)
|
||||
/x',
|
||||
\Closure::fromCallable([$this, 'getPropertyValueReplacement']),
|
||||
$propertyValue
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<non-empty-string, string> $declarations
|
||||
*
|
||||
* @return ?array<non-empty-string, string> `null` is returned if no substitutions were made.
|
||||
*/
|
||||
private function replaceVariablesInDeclarations(array $declarations): ?array
|
||||
{
|
||||
$substitutionsMade = false;
|
||||
$result = \array_map(
|
||||
function (string $propertyValue) use (&$substitutionsMade): string {
|
||||
$newPropertyValue = $this->replaceVariablesInPropertyValue($propertyValue);
|
||||
if ($newPropertyValue !== $propertyValue) {
|
||||
$substitutionsMade = true;
|
||||
}
|
||||
return $newPropertyValue;
|
||||
},
|
||||
$declarations
|
||||
);
|
||||
|
||||
return $substitutionsMade ? $result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<non-empty-string, string> $declarations
|
||||
*/
|
||||
private function getDeclarationsAsString(array $declarations): string
|
||||
{
|
||||
$declarationStrings = \array_map(
|
||||
static function (string $key, string $value): string {
|
||||
return $key . ': ' . $value;
|
||||
},
|
||||
\array_keys($declarations),
|
||||
\array_values($declarations)
|
||||
);
|
||||
|
||||
return \implode('; ', $declarationStrings) . ';';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<non-empty-string, string> $ancestorVariableDefinitions
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
private function evaluateVariablesInElementAndDescendants(
|
||||
\DOMElement $element,
|
||||
array $ancestorVariableDefinitions
|
||||
): self {
|
||||
$style = $element->getAttribute('style');
|
||||
|
||||
// Avoid parsing declarations if none use or define a variable
|
||||
if ((new Preg())->match('/(?<![\\w\\-])--[\\w\\-]/', $style) !== 0) {
|
||||
$declarations = (new DeclarationBlockParser())->parse($style);
|
||||
$variableDefinitions = $this->currentVariableDefinitions
|
||||
= $this->getVariableDefinitionsFromDeclarations($declarations) + $ancestorVariableDefinitions;
|
||||
|
||||
$newDeclarations = $this->replaceVariablesInDeclarations($declarations);
|
||||
if ($newDeclarations !== null) {
|
||||
$element->setAttribute('style', $this->getDeclarationsAsString($newDeclarations));
|
||||
}
|
||||
} else {
|
||||
$variableDefinitions = $ancestorVariableDefinitions;
|
||||
}
|
||||
|
||||
foreach ($element->childNodes as $child) {
|
||||
if ($child instanceof \DOMElement) {
|
||||
$this->evaluateVariablesInElementAndDescendants($child, $variableDefinitions);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pelago\Emogrifier\Utilities;
|
||||
|
||||
/**
|
||||
* Provides a common method for parsing CSS declaration blocks.
|
||||
* These might be from actual CSS, or from the `style` attribute of an HTML DOM element.
|
||||
*
|
||||
* Caches results globally.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class DeclarationBlockParser
|
||||
{
|
||||
/**
|
||||
* @var array<string, array<non-empty-string, string>>
|
||||
*/
|
||||
private static $cache = [];
|
||||
|
||||
/**
|
||||
* CSS custom properties (variables) have case-sensitive names, so their case must be preserved.
|
||||
* Standard CSS properties have case-insensitive names, which are converted to lowercase.
|
||||
*
|
||||
* @param non-empty-string $name
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function normalizePropertyName(string $name): string
|
||||
{
|
||||
if (\substr($name, 0, 2) === '--') {
|
||||
return $name;
|
||||
} else {
|
||||
return \strtolower($name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a CSS declaration block into property name/value pairs.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* The declaration block
|
||||
*
|
||||
* "color: #000; font-weight: bold;"
|
||||
*
|
||||
* will be parsed into the following array:
|
||||
*
|
||||
* "color" => "#000"
|
||||
* "font-weight" => "bold"
|
||||
*
|
||||
* @param string $declarationBlock the CSS declarations block without the curly braces, may be empty
|
||||
*
|
||||
* @return array<non-empty-string, string>
|
||||
* the CSS declarations with the property names as array keys and the property values as array values
|
||||
*
|
||||
* @throws \UnexpectedValueException if an empty property name is encountered (which cannot happen)
|
||||
*/
|
||||
public function parse(string $declarationBlock): array
|
||||
{
|
||||
if (isset(self::$cache[$declarationBlock])) {
|
||||
return self::$cache[$declarationBlock];
|
||||
}
|
||||
|
||||
$preg = new Preg();
|
||||
|
||||
$declarations = $preg->split('/;(?!base64|charset)/', $declarationBlock);
|
||||
|
||||
$properties = [];
|
||||
foreach ($declarations as $declaration) {
|
||||
$matches = [];
|
||||
if (
|
||||
$preg->match(
|
||||
'/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s',
|
||||
\trim($declaration),
|
||||
$matches
|
||||
)
|
||||
=== 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$propertyName = $matches[1];
|
||||
if ($propertyName === '') {
|
||||
// This cannot happen since the regular epression matches one or more characters.
|
||||
throw new \UnexpectedValueException('An empty property name was encountered.', 1727046409);
|
||||
}
|
||||
$propertyValue = $matches[2];
|
||||
$properties[$this->normalizePropertyName($propertyName)] = $propertyValue;
|
||||
}
|
||||
self::$cache[$declarationBlock] = $properties;
|
||||
|
||||
return $properties;
|
||||
}
|
||||
}
|
||||
202
lib/pelago/emogrifier/src/Utilities/Preg.php
Normal file
202
lib/pelago/emogrifier/src/Utilities/Preg.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pelago\Emogrifier\Utilities;
|
||||
|
||||
/**
|
||||
* PHP's `preg_*` functions can return `false` on failure.
|
||||
* Failure is rare but may occur with a complex pattern applied to a long subject.
|
||||
* Catastrophic backtracking may occur ({@see https://www.regular-expressions.info/catastrophic.html}).
|
||||
* Failure may also occur due to programmer error, if an invalid pattern is provided.
|
||||
*
|
||||
* Catering for failure in each case clutters up the code with error handling.
|
||||
* This class provides wrappers for some `preg_*` functions, with errors handled either
|
||||
* - by throwing an exception, or
|
||||
* - by triggering a user error and providing fallback logic (e.g. returning the subject string unmodified).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Preg
|
||||
{
|
||||
/**
|
||||
* whether to throw exceptions on errors (or call `trigger_error` and implement fallback)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $throwExceptions = false;
|
||||
|
||||
/**
|
||||
* Sets whether exceptions should be thrown if an error occurs.
|
||||
*/
|
||||
public function throwExceptions(bool $throw): self
|
||||
{
|
||||
$this->throwExceptions = $throw;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps `preg_replace`, though does not support `$subject` being an array.
|
||||
* If an error occurs, and exceptions are not being thrown, the original `$subject` is returned.
|
||||
*
|
||||
* @param non-empty-string|non-empty-array<non-empty-string> $pattern
|
||||
* @param string|non-empty-array<string> $replacement
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function replace($pattern, $replacement, string $subject, int $limit = -1, ?int &$count = null): string
|
||||
{
|
||||
$result = \preg_replace($pattern, $replacement, $subject, $limit, $count);
|
||||
|
||||
if ($result === null) {
|
||||
$this->logOrThrowPregLastError();
|
||||
$result = $subject;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps `preg_replace_callback`, though does not support `$subject` being an array.
|
||||
* If an error occurs, and exceptions are not being thrown, the original `$subject` is returned.
|
||||
*
|
||||
* Note that (unlike when calling `preg_replace_callback`), `$callback` cannot be a non-public method
|
||||
* represented by an array comprising an object or class name and the method name.
|
||||
* To circumvent that, use `\Closure::fromCallable([$objectOrClassName, 'method'])`.
|
||||
*
|
||||
* @param non-empty-string|non-empty-array<non-empty-string> $pattern
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function replaceCallback(
|
||||
$pattern,
|
||||
callable $callback,
|
||||
string $subject,
|
||||
int $limit = -1,
|
||||
?int &$count = null
|
||||
): string {
|
||||
$result = \preg_replace_callback($pattern, $callback, $subject, $limit, $count);
|
||||
|
||||
if ($result === null) {
|
||||
$this->logOrThrowPregLastError();
|
||||
$result = $subject;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps `preg_split`.
|
||||
* If an error occurs, and exceptions are not being thrown,
|
||||
* a single-element array containing the original `$subject` is returned.
|
||||
* This method does not support the `PREG_SPLIT_OFFSET_CAPTURE` flag and will throw an exception if it is specified.
|
||||
*
|
||||
* @param non-empty-string $pattern
|
||||
*
|
||||
* @return array<int, string>
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function split(string $pattern, string $subject, int $limit = -1, int $flags = 0): array
|
||||
{
|
||||
if (($flags & PREG_SPLIT_OFFSET_CAPTURE) !== 0) {
|
||||
throw new \RuntimeException('PREG_SPLIT_OFFSET_CAPTURE is not supported by Preg::split', 1726506348);
|
||||
}
|
||||
|
||||
$result = \preg_split($pattern, $subject, $limit, $flags);
|
||||
|
||||
if ($result === false) {
|
||||
$this->logOrThrowPregLastError();
|
||||
$result = [$subject];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps `preg_match`.
|
||||
* If an error occurs, and exceptions are not being thrown,
|
||||
* zero (`0`) is returned, and if the `$matches` parameter is provided, it is set to an empty array.
|
||||
* This method does not currently support the `$flags` or `$offset` parameters.
|
||||
*
|
||||
* @param non-empty-string $pattern
|
||||
* @param array<int, string> $matches
|
||||
*
|
||||
* @return 0|1
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function match(string $pattern, string $subject, ?array &$matches = null): int
|
||||
{
|
||||
$result = \preg_match($pattern, $subject, $matches);
|
||||
|
||||
if ($result === false) {
|
||||
$this->logOrThrowPregLastError();
|
||||
$result = 0;
|
||||
$matches = [];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps `preg_match_all`.
|
||||
*
|
||||
* If an error occurs, and exceptions are not being thrown, zero (`0`) is returned.
|
||||
*
|
||||
* In the error case, if the `$matches` parameter is provided, it is set to an array containing empty arrays for the
|
||||
* full pattern match and any possible subpattern match that might be expected.
|
||||
* The algorithm to determine the length of this array simply counts the number of opening parentheses in the
|
||||
* `$pattern`, which may result in a longer array than expected, but guarantees that it is at least as long as
|
||||
* expected.
|
||||
*
|
||||
* This method does not currently support the `$flags` or `$offset` parameters.
|
||||
*
|
||||
* @param non-empty-string $pattern
|
||||
* @param array<int, array<int, string>> $matches
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function matchAll(string $pattern, string $subject, ?array &$matches = null): int
|
||||
{
|
||||
$result = \preg_match_all($pattern, $subject, $matches);
|
||||
|
||||
if ($result === false) {
|
||||
$this->logOrThrowPregLastError();
|
||||
$result = 0;
|
||||
$matches = \array_fill(0, \substr_count($pattern, '(') + 1, []);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the name of the error constant for `preg_last_error`
|
||||
* (based on code posted at {@see https://www.php.net/manual/en/function.preg-last-error.php#124124})
|
||||
* and puts it into an error message which is either passed to `trigger_error`
|
||||
* or used in the exception which is thrown (depending on the `$throwExceptions` property).
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
private function logOrThrowPregLastError(): void
|
||||
{
|
||||
$pcreConstants = \get_defined_constants(true)['pcre'];
|
||||
$pcreErrorConstantNames = \array_flip(\array_filter(
|
||||
$pcreConstants,
|
||||
static function (string $key): bool {
|
||||
return \substr($key, -6) === '_ERROR';
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
));
|
||||
|
||||
$pregLastError = \preg_last_error();
|
||||
$message = 'PCRE regex execution error `' . (string) ($pcreErrorConstantNames[$pregLastError] ?? $pregLastError)
|
||||
. '`';
|
||||
|
||||
if ($this->throwExceptions) {
|
||||
throw new \RuntimeException($message, 1592870147);
|
||||
}
|
||||
\trigger_error($message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user