Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitlab/review-prompt.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
You are a code reviewer for the **AtroCore** project (open-source PIM on PHP 8.1+).
You are a code reviewer for the **AtroCore** project (open-source PIM on PHP 8.4+).

## Project Context

Expand Down
2 changes: 1 addition & 1 deletion app/Atro/ConnectionType/ConnectionSmtp.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function connect(Entity $connectionEntity): TransportInterface
$authType = $connectionEntity->get('smtpAuthType');

if (empty($authType) || $authType == 'basic') {
$scheme = in_array($connectionEntity->get('smtpSecurity'), ['SSL', 'TLS']) ? ($connectionEntity->get('smtpPort') === 465 ? 'smtps' : 'smtp') : '';
$scheme = in_array($connectionEntity->get('smtpSecurity'), ['SSL', 'TLS']) && $connectionEntity->get('smtpPort') === 465 ? 'smtps' : 'smtp';
return $factory->create(new Dsn(
$scheme,
$connectionEntity->get('smtpServer') ?? '',
Expand Down
27 changes: 26 additions & 1 deletion app/Atro/Core/Utils/HTMLSanitizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,20 @@

use Symfony\Component\HtmlSanitizer\HtmlSanitizer as BaseHtmlSanitizer;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
use Symfony\Component\HtmlSanitizer\Parser\MastermindsParser;
use Symfony\Component\HtmlSanitizer\Parser\NativeParser;
use Symfony\Component\HtmlSanitizer\Parser\ParserInterface;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;

class HTMLSanitizer
{
public const string LEGACY_PARSER_CONFIG_KEY = 'htmlSanitizerLegacyParser';

public function __construct(private readonly ?Config $config = null)
{
}

public function sanitize(string $content, string $paramsString): string
{
if (empty($content) || empty($paramsString)) {
Expand All @@ -29,7 +38,7 @@ public function sanitize(string $content, string $paramsString): string
return $content;
}

$sanitizer = new BaseHtmlSanitizer($this->getConfig($params));
$sanitizer = new BaseHtmlSanitizer($this->getConfig($params), $this->createParser());

try {
$sanitized = $sanitizer->sanitize($content);
Expand Down Expand Up @@ -73,6 +82,22 @@ protected function getConfig(array $params): HtmlSanitizerConfig
return $config;
}

public static function isParserConfigurable(): bool
{
return class_exists(MastermindsParser::class) && class_exists(NativeParser::class);
}

protected function createParser(): ?ParserInterface
{
if (!self::isParserConfigurable()) {
return null;
}

$useLegacyParser = $this->config === null || (bool)($this->config->get(self::LEGACY_PARSER_CONFIG_KEY) ?? true);

return $useLegacyParser ? new MastermindsParser() : new NativeParser();
}

protected function blockElements(HtmlSanitizerConfig $config, $params): HtmlSanitizerConfig
{
if (is_array($params)) {
Expand Down
173 changes: 173 additions & 0 deletions app/Atro/Core/Utils/YamlDuplicateKeys.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<?php
/**
* AtroCore Software
*
* This source file is available under GNU General Public License version 3 (GPLv3).
* Full copyright and license information is available in LICENSE.txt, located in the root directory.
*
* @copyright Copyright (c) AtroCore GmbH (https://www.atrocore.com)
* @license GPLv3 (https://www.gnu.org/licenses/)
*/

declare(strict_types=1);

namespace Atro\Core\Utils;

use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;

/**
* Symfony Yaml 8.0 rejects a duplicate mapping key whose first occurrence is null, which earlier
* versions accepted with the last occurrence winning. Dropping that first occurrence keeps the parsed
* result identical, and a repair is only returned when parsing again proves it.
*/
class YamlDuplicateKeys
{
private const array NULL_VALUES = ['', '~', 'null'];

private array $indents = [];

private array $keys = [];

private array $values = [];

private array $droppedKeys = [];

public function find(string $yaml): array
{
return $this->keysOf($this->findCandidates($yaml));
}

public function remove(string $yaml): ?string
{
$this->droppedKeys = [];

$candidates = $this->findCandidates($yaml);
if ($candidates === []) {
return null;
}

$lines = explode("\n", $yaml);
$parsed = $this->parse($yaml);

if ($parsed !== null) {
$repaired = $this->withoutLines($lines, $candidates);

if ($this->parse($repaired) !== $parsed) {
return null;
}

$this->droppedKeys = $this->keysOf($candidates);

return $repaired;
}

$removed = [];
foreach ($candidates as $index) {
$removed[] = $index;
$repaired = $this->withoutLines($lines, $removed);

if ($this->parse($repaired) !== null) {
$this->droppedKeys = $this->keysOf($removed);

return $repaired;
}
}

return null;
}

public function getDroppedKeys(): array
{
return $this->droppedKeys;
}

private function findCandidates(string $yaml): array
{
$this->indexLines(explode("\n", $yaml));

$candidates = [];
foreach ($this->keys as $index => $key) {
if (!in_array($this->values[$index], self::NULL_VALUES, true)) {
continue;
}

if (!$this->hasNestedBlock($index) && $this->hasSiblingDuplicate($index)) {
$candidates[] = $index;
}
}

return $candidates;
}

private function indexLines(array $lines): void
{
$this->indents = [];
$this->keys = [];
$this->values = [];

foreach ($lines as $index => $line) {
if (trim($line) === '' || str_starts_with(trim($line), '#')) {
continue;
}

$this->indents[$index] = strlen($line) - strlen(ltrim($line, ' '));

if (preg_match('/^ *([A-Za-z_][A-Za-z0-9_.-]*) *:(.*)$/', $line, $match) === 1) {
$this->keys[$index] = $match[1];
$this->values[$index] = strtolower(trim($match[2]));
}
}
}

private function hasSiblingDuplicate(int $index): bool
{
foreach ($this->indents as $candidate => $indent) {
if ($candidate <= $index) {
continue;
}

if ($indent < $this->indents[$index]) {
return false;
}

if ($indent === $this->indents[$index] && ($this->keys[$candidate] ?? null) === $this->keys[$index]) {
return true;
}
}

return false;
}

private function hasNestedBlock(int $index): bool
{
foreach ($this->indents as $candidate => $indent) {
if ($candidate > $index) {
return $indent > $this->indents[$index];
}
}

return false;
}

private function withoutLines(array $lines, array $indexes): string
{
return implode("\n", array_values(array_diff_key($lines, array_fill_keys($indexes, true))));
}

private function keysOf(array $indexes): array
{
return array_values(array_unique(array_map(fn(int $index): string => $this->keys[$index], $indexes)));
}

private function parse(string $yaml): ?array
{
try {
$parsed = @Yaml::parse($yaml);
} catch (ParseException) {
return null;
}

return is_array($parsed) ? $parsed : null;
}
}
50 changes: 50 additions & 0 deletions app/Atro/Listeners/SettingsLayout.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php
/**
* AtroCore Software
*
* This source file is available under GNU General Public License version 3 (GPLv3).
* Full copyright and license information is available in LICENSE.txt, located in the root directory.
*
* @copyright Copyright (c) AtroCore GmbH (https://www.atrocore.com)
* @license GPLv3 (https://www.gnu.org/licenses/)
*/

declare(strict_types=1);

namespace Atro\Listeners;

use Atro\Core\EventManager\Event;
use Atro\Core\Utils\HTMLSanitizer;

class SettingsLayout extends AbstractLayoutListener
{
public function settings(Event $event): void
{
$layout = $event->getArgument('result');

if (HTMLSanitizer::isParserConfigurable() || !is_array($layout)) {
return;
}

$event->setArgument('result', $this->hideField($layout, HTMLSanitizer::LEGACY_PARSER_CONFIG_KEY));
}

protected function hideField(array $layout, string $name): array
{
foreach ($layout as $panelKey => $panel) {
foreach ($panel['rows'] ?? [] as $rowKey => $row) {
if (!is_array($row)) {
continue;
}

foreach ($row as $cellKey => $cell) {
if (is_array($cell) && ($cell['name'] ?? null) === $name) {
$layout[$panelKey]['rows'][$rowKey][$cellKey] = false;
}
}
}
}

return $layout;
}
}
Loading