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:
Stephen Abello
2025-09-17 16:04:20 +02:00
committed by GitHub
parent 8982f7e0e3
commit 428d2c6356
645 changed files with 20889 additions and 81911 deletions

View 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;
}
}

View File

@@ -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;
}
}

View 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);
}
}