Release 2.47 (#5784)
This commit is contained in:
@@ -508,7 +508,7 @@ final class TranslationCommand extends Command
|
||||
\sprintf('Missing english translation for key: %s in file %s', $key, $file)
|
||||
);
|
||||
}
|
||||
$unit->target[0] = $translations[$key];
|
||||
$unit->target[0] = $translations[$key]; // @phpstan-ignore-line
|
||||
$unit->target['state'] = 'needs-translation';
|
||||
$foundEmpty = true;
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ final class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.46.0';
|
||||
public const VERSION = '2.47.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 24600;
|
||||
public const VERSION_ID = 24700;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -301,7 +301,7 @@ class InvoiceTemplate implements EntityWithMetaFields
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection|MetaTableTypeInterface[]
|
||||
* @return Collection<int, InvoiceTemplateMeta>
|
||||
*/
|
||||
public function getMetaFields(): Collection
|
||||
{
|
||||
|
||||
@@ -376,14 +376,24 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called from the "edit user preferences" form.
|
||||
* Therefor it just merges the values for existing preferences and adds new ones.
|
||||
* But it will NOT remove existing preferences or replace the underlying collection.
|
||||
*
|
||||
* @param iterable<UserPreference> $preferences
|
||||
*/
|
||||
public function setPreferences(iterable $preferences): User
|
||||
{
|
||||
$this->preferences = new ArrayCollection();
|
||||
|
||||
foreach ($preferences as $preference) {
|
||||
$this->addPreference($preference);
|
||||
if (($name = $preference->getName()) === null) {
|
||||
continue;
|
||||
}
|
||||
$p = $this->getPreference($name);
|
||||
if ($p === null) {
|
||||
$this->addPreference($preference);
|
||||
} else {
|
||||
$p->setValue($preference->getValue());
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
||||
@@ -24,7 +24,7 @@ use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Twig\SecurityPolicy\ExportPolicy;
|
||||
use App\Twig\SecurityPolicy\StrictPolicy;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -100,9 +100,12 @@ class HtmlRenderer implements ExportRendererInterface
|
||||
$summary = $this->calculateSummary($exportItems);
|
||||
|
||||
// enable basic security measures
|
||||
$sandbox = new SandboxExtension(new ExportPolicy());
|
||||
if (!$this->twig->hasExtension(SandboxExtension::class)) {
|
||||
$this->twig->addExtension(new SandboxExtension(new StrictPolicy()));
|
||||
}
|
||||
|
||||
$sandbox = $this->twig->getExtension(SandboxExtension::class);
|
||||
$sandbox->enableSandbox();
|
||||
$this->twig->addExtension($sandbox);
|
||||
|
||||
$content = $this->twig->render($this->getTemplate(), array_merge([
|
||||
'entries' => $exportItems,
|
||||
|
||||
@@ -17,7 +17,7 @@ use App\Pdf\PdfContext;
|
||||
use App\Pdf\PdfRendererTrait;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Twig\SecurityPolicy\ExportPolicy;
|
||||
use App\Twig\SecurityPolicy\StrictPolicy;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Twig\Environment;
|
||||
@@ -103,9 +103,12 @@ class PDFRenderer implements DispositionInlineInterface, ExportRendererInterface
|
||||
$summary = $this->calculateSummary($exportItems);
|
||||
|
||||
// enable basic security measures
|
||||
$sandbox = new SandboxExtension(new ExportPolicy());
|
||||
if (!$this->twig->hasExtension(SandboxExtension::class)) {
|
||||
$this->twig->addExtension(new SandboxExtension(new StrictPolicy()));
|
||||
}
|
||||
|
||||
$sandbox = $this->twig->getExtension(SandboxExtension::class);
|
||||
$sandbox->enableSandbox();
|
||||
$this->twig->addExtension($sandbox);
|
||||
|
||||
$content = $this->twig->render($this->getTemplate(), array_merge([
|
||||
'entries' => $exportItems,
|
||||
@@ -116,6 +119,8 @@ class PDFRenderer implements DispositionInlineInterface, ExportRendererInterface
|
||||
'pdfContext' => $context
|
||||
], $this->getOptions($query)));
|
||||
|
||||
$sandbox->disableSandbox();
|
||||
|
||||
$pdfOptions = array_merge($context->getOptions(), $this->getPdfOptions());
|
||||
|
||||
$content = $this->converter->convertToPdf($content, $pdfOptions);
|
||||
|
||||
@@ -30,11 +30,10 @@ final class UserPreferencesCollectionType extends AbstractType
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SET_DATA,
|
||||
function (FormEvent $event): void {
|
||||
/** @var ArrayCollection<UserPreference> $collection */
|
||||
/** @var ArrayCollection<int, UserPreference> $collection */
|
||||
$collection = $event->getData();
|
||||
$preferences = new ArrayCollection();
|
||||
foreach ($collection as $collectionItem) {
|
||||
$collection->removeElement($collectionItem);
|
||||
|
||||
if (!($collectionItem instanceof UserPreference)) {
|
||||
continue;
|
||||
}
|
||||
@@ -44,8 +43,9 @@ final class UserPreferencesCollectionType extends AbstractType
|
||||
continue;
|
||||
}
|
||||
|
||||
$collection->set($collectionItem->getName(), $collectionItem);
|
||||
$preferences->set($collectionItem->getName(), clone $collectionItem);
|
||||
}
|
||||
$event->setData($preferences);
|
||||
},
|
||||
// must be a higher priority then the listener in UserPreferenceType
|
||||
100
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\RendererInterface;
|
||||
use App\Model\InvoiceDocument;
|
||||
use App\Twig\LocaleFormatExtensions;
|
||||
use App\Twig\SecurityPolicy\InvoicePolicy;
|
||||
use App\Twig\SecurityPolicy\StrictPolicy;
|
||||
use Symfony\Bridge\Twig\Extension\TranslationExtension;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Twig\Environment;
|
||||
@@ -64,7 +64,7 @@ abstract class AbstractTwigRenderer implements RendererInterface
|
||||
}
|
||||
|
||||
if (!$twig->hasExtension(SandboxExtension::class)) {
|
||||
$twig->addExtension(new SandboxExtension(new InvoicePolicy()));
|
||||
$twig->addExtension(new SandboxExtension(new StrictPolicy()));
|
||||
}
|
||||
|
||||
$sandbox = $twig->getExtension(SandboxExtension::class);
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
final class ChainPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
/** @var array<SecurityPolicyInterface> */
|
||||
private array $policies = [];
|
||||
|
||||
public function addPolicy(SecurityPolicyInterface $policy): void
|
||||
{
|
||||
$this->policies[] = $policy;
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
foreach ($this->policies as $policy) {
|
||||
$policy->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
foreach ($this->policies as $policy) {
|
||||
$policy->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
foreach ($this->policies as $policy) {
|
||||
$policy->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Pdf\PdfContext;
|
||||
use Symfony\Bridge\Twig\AppVariable;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\ServerBag;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
/**
|
||||
* The Twig environment needs the sandbox extension, which itself needs a policy to start working.
|
||||
*/
|
||||
final class DefaultPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
if ($obj instanceof ServerBag) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access server environment', ServerBag::class, $method);
|
||||
}
|
||||
|
||||
if ($obj instanceof SessionInterface) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access session', SessionInterface::class, $method);
|
||||
}
|
||||
|
||||
$lcm = strtolower($method);
|
||||
|
||||
if ($obj instanceof PdfContext) {
|
||||
if ($lcm !== 'setoption') {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access forbidden method on PdfContext', PdfContext::class, $method);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!str_starts_with($lcm, 'has') && !str_starts_with($lcm, 'is') && !str_starts_with($lcm, 'get') && $lcm !== '__tostring') {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access non-read method', $obj::class, $method);
|
||||
}
|
||||
|
||||
if ($obj instanceof Request) {
|
||||
if (!str_starts_with($lcm, 'get')) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to call setter() of app variable', AppVariable::class, $method);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($obj instanceof AppVariable) {
|
||||
if (!\in_array($lcm, ['getrequest', 'getuser', 'getlocale'], true)) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access forbidden app variable method', User::class, $method);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($obj instanceof User) {
|
||||
if (\in_array($lcm, ['getpassword', 'gettotpsecret', 'getplainpassword', 'getconfirmationtoken', 'gettotpauthenticationconfiguration'], true)) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access user secrets', User::class, $method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
/**
|
||||
* Represents the security policy for custom Twig export templates.
|
||||
*/
|
||||
final class ExportPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
private ChainPolicy $policy;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->policy = new ChainPolicy();
|
||||
$this->policy->addPolicy(new DefaultPolicy());
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
$this->policy->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
$this->policy->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$this->policy->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Pdf\PdfContext;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
use Twig\Markup;
|
||||
use Twig\Sandbox\SecurityPolicy;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
use Twig\Template;
|
||||
|
||||
/**
|
||||
* Represents the security policy for custom Twig invoice templates.
|
||||
*/
|
||||
final class InvoicePolicy implements SecurityPolicyInterface
|
||||
{
|
||||
private SecurityPolicyInterface $default;
|
||||
private SecurityPolicyInterface $security;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->default = new DefaultPolicy();
|
||||
$this->security = new SecurityPolicy(
|
||||
['block', 'if', 'for', 'set', 'extends', 'import'],
|
||||
[
|
||||
// =================================================================
|
||||
// vendor/twig/twig/src/Extension/CoreExtension.php
|
||||
|
||||
// formatting filters
|
||||
'date',
|
||||
'date_modify',
|
||||
'format',
|
||||
'replace',
|
||||
'number_format',
|
||||
'abs',
|
||||
'round',
|
||||
|
||||
// encoding
|
||||
'url_encode',
|
||||
'json_encode',
|
||||
'convert_encoding',
|
||||
|
||||
// string filters
|
||||
'title',
|
||||
'capitalize',
|
||||
'upper',
|
||||
'lower',
|
||||
'striptags',
|
||||
'trim',
|
||||
'nl2br',
|
||||
'spaceless',
|
||||
|
||||
// array helpers
|
||||
'join',
|
||||
'split',
|
||||
'sort',
|
||||
'merge',
|
||||
'batch',
|
||||
'column',
|
||||
'filter',
|
||||
'map',
|
||||
'reduce',
|
||||
|
||||
// string/array filters
|
||||
'reverse',
|
||||
'length',
|
||||
'slice',
|
||||
'first',
|
||||
'last',
|
||||
|
||||
// iteration and runtime
|
||||
'default',
|
||||
'keys',
|
||||
|
||||
// =================================================================
|
||||
// vendor/twig/twig/src/Extension/EscaperExtension.php
|
||||
'escape',
|
||||
'e',
|
||||
'raw',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/twig-bridge/Extension/TranslationExtension.php
|
||||
'trans',
|
||||
|
||||
// =================================================================
|
||||
// vendor/twig/string-extra/StringExtension.php
|
||||
'u',
|
||||
'slug',
|
||||
|
||||
// =================================================================
|
||||
// vendor/twig/intl-extra/IntlExtension.php
|
||||
'country_name',
|
||||
'currency_name',
|
||||
'currency_symbol',
|
||||
'language_name',
|
||||
'locale_name',
|
||||
'timezone_name',
|
||||
'format_currency',
|
||||
'format_number',
|
||||
'format_decimal_number',
|
||||
'format_currency_number',
|
||||
'format_percent_number',
|
||||
'format_scientific_number',
|
||||
'format_spellout_number',
|
||||
'format_ordinal_number',
|
||||
'format_duration_number',
|
||||
'format_datetime',
|
||||
'format_date',
|
||||
'format_time',
|
||||
|
||||
// =================================================================
|
||||
// src/Twig/LocaleFormatExtensions.php
|
||||
'month_name',
|
||||
'day_name',
|
||||
'date_short',
|
||||
'date_time',
|
||||
'date_full',
|
||||
'date_format',
|
||||
'date_weekday',
|
||||
'time',
|
||||
'duration',
|
||||
'duration_decimal',
|
||||
'money',
|
||||
'amount',
|
||||
|
||||
// =================================================================
|
||||
// src/Twig/RuntimeExtensions.php
|
||||
'md2html',
|
||||
'desc2html',
|
||||
'comment2html',
|
||||
'comment1line',
|
||||
|
||||
// =================================================================
|
||||
// src/Twig/Extensions.php
|
||||
'multiline_indent',
|
||||
'color',
|
||||
'font_contrast',
|
||||
'default_color',
|
||||
'nl2str',
|
||||
],
|
||||
[
|
||||
PdfContext::class => ['setoption'],
|
||||
InvoiceModel::class => ['toarray'],
|
||||
],
|
||||
[], // properties
|
||||
[
|
||||
// =================================================================
|
||||
// vendor/twig/twig/src/Extension/CoreExtension.php
|
||||
'max',
|
||||
'min',
|
||||
'range',
|
||||
'constant',
|
||||
'cycle',
|
||||
'random',
|
||||
'date',
|
||||
'asset',
|
||||
'range',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/twig-bridge/Extension/TranslationExtension.php
|
||||
't',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/webpack-encore-bundle/src/Twig/EntryFilesTwigExtension.php
|
||||
'encore_entry_css_source',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/twig-bridge/Extension/AssetExtension.php
|
||||
'asset',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/twig-bridge/Extension/SecurityExtension.php
|
||||
'is_granted',
|
||||
|
||||
// =================================================================
|
||||
// Twig/RuntimeExtensions.php
|
||||
'qr_code_data_uri',
|
||||
|
||||
// =================================================================
|
||||
// Twig/Configuration.php
|
||||
'config',
|
||||
|
||||
// =================================================================
|
||||
// Twig/LocaleFormatExtensions.php
|
||||
'create_date',
|
||||
'month_names',
|
||||
'locale_format',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
$this->default->checkSecurity($tags, $filters, $functions);
|
||||
$this->security->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
if ($obj instanceof Template || $obj instanceof Markup || $obj instanceof UnicodeString) { // @phpstan-ignore instanceof.internalClass
|
||||
return;
|
||||
}
|
||||
|
||||
$this->default->checkMethodAllowed($obj, $method);
|
||||
|
||||
$lm = strtolower($method);
|
||||
|
||||
if (str_starts_with($lm, 'get') || str_starts_with($lm, 'is') || str_starts_with($lm, 'has') || $lm === '__tostring') {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->security->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$this->default->checkPropertyAllowed($obj, $property);
|
||||
$this->security->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
165
src/Twig/SecurityPolicy/StrictPolicy.php
Normal file
165
src/Twig/SecurityPolicy/StrictPolicy.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\User;
|
||||
use App\Pdf\PdfContext;
|
||||
use Symfony\Bridge\Twig\AppVariable;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\ServerBag;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
use Twig\Sandbox\SecurityNotAllowedFilterError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFunctionError;
|
||||
use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
use Twig\Sandbox\SecurityNotAllowedTagError;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
/**
|
||||
* The Twig environment needs the sandbox extension, which itself needs a policy to start working.
|
||||
*/
|
||||
final class StrictPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $allowedTags = ['block', 'if', 'for', 'set', 'macro', 'import', 'extends', 'from'];
|
||||
/** @var string[] */
|
||||
private array $allowedFunctions = [
|
||||
// vendor/twig/twig/src/Extension/CoreExtension.php
|
||||
'max', 'min', 'range', 'constant', 'cycle', 'random', 'date',
|
||||
// vendor/symfony/twig-bridge/Extension/TranslationExtension.php
|
||||
't',
|
||||
// vendor/symfony/webpack-encore-bundle/src/Twig/EntryFilesTwigExtension.php
|
||||
'encore_entry_css_source', 'encore_entry_link_tags', 'encore_entry_script_tags',
|
||||
// vendor/symfony/twig-bridge/Extension/SecurityExtension.php
|
||||
'is_granted',
|
||||
// Twig/RuntimeExtensions.php
|
||||
'qr_code_data_uri',
|
||||
// Twig/Configuration.php
|
||||
'config',
|
||||
// Twig/LocaleFormatExtensions.php
|
||||
'create_date', 'month_names', 'locale_format',
|
||||
// Twig/Extensions.php
|
||||
'class_name'
|
||||
];
|
||||
/** @var string[] */
|
||||
private array $allowedFilters = [
|
||||
// vendor/twig/twig/src/Extension/CoreExtension.php
|
||||
// formatting filters
|
||||
'date', 'date_modify', 'format', 'replace', 'number_format', 'abs', 'round',
|
||||
// encoding
|
||||
'url_encode', 'json_encode',
|
||||
// string filters
|
||||
'title', 'capitalize', 'upper', 'lower', 'striptags', 'trim', 'nl2br', 'spaceless',
|
||||
// array helpers
|
||||
'join', 'split', 'sort', 'merge', 'column', 'filter', 'map',
|
||||
// string/array filters
|
||||
'reverse', 'length', 'slice', 'first', 'last',
|
||||
// iteration and runtime
|
||||
'default', 'keys',
|
||||
// vendor/twig/twig/src/Extension/EscaperExtension.php
|
||||
'escape', 'e', 'raw',
|
||||
// vendor/symfony/twig-bridge/Extension/TranslationExtension.php
|
||||
'trans',
|
||||
// vendor/twig/string-extra/StringExtension.php
|
||||
'u', 'slug',
|
||||
// vendor/twig/intl-extra/IntlExtension.php
|
||||
'country_name', 'currency_name', 'currency_symbol', 'language_name', 'locale_name', 'timezone_name',
|
||||
'format_currency', 'format_number', 'format_decimal_number', 'format_currency_number',
|
||||
'format_duration_number', 'format_datetime', 'format_date', 'format_time',
|
||||
// src/Twig/LocaleFormatExtensions.php
|
||||
'month_name', 'day_name', 'date_short', 'date_time', 'date_full', 'date_format',
|
||||
'date_weekday', 'time', 'duration', 'duration_decimal', 'money', 'amount',
|
||||
// src/Twig/RuntimeExtensions.php
|
||||
'md2html', 'desc2html', 'comment2html', 'comment1line',
|
||||
// src/Twig/Extensions.php
|
||||
'multiline_indent', 'color', 'nl2str'
|
||||
];
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
foreach ($tags as $tag) {
|
||||
if (!\in_array($tag, $this->allowedTags, true)) {
|
||||
throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
if (!\in_array($filter, $this->allowedFilters, true)) {
|
||||
throw new SecurityNotAllowedFilterError(\sprintf('Filter "%s" is not allowed.', $filter), $filter);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($functions as $function) {
|
||||
if (!\in_array($function, $this->allowedFunctions, true)) {
|
||||
throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
if ($obj instanceof UnicodeString) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($obj instanceof ServerBag) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access server environment', ServerBag::class, $method);
|
||||
}
|
||||
|
||||
if ($obj instanceof SessionInterface) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access session', SessionInterface::class, $method);
|
||||
}
|
||||
|
||||
$lcm = strtolower($method);
|
||||
|
||||
if ($obj instanceof PdfContext) {
|
||||
if ($lcm !== 'setoption') {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access forbidden method on PdfContext', PdfContext::class, $method);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($obj instanceof MetaTableTypeInterface && $lcm === 'merge') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($obj instanceof Request) {
|
||||
if (!str_starts_with($lcm, 'get')) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to call setter() of app variable', AppVariable::class, $method);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($obj instanceof AppVariable) {
|
||||
if (!\in_array($lcm, ['getrequest', 'getuser', 'getlocale'], true)) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access forbidden app variable method', User::class, $method);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!str_starts_with($lcm, 'get') && !str_starts_with($lcm, 'has') && !str_starts_with($lcm, 'is') && $lcm !== '__tostring') {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access non-read method', $obj::class, $method);
|
||||
}
|
||||
|
||||
if ($obj instanceof User) {
|
||||
if (\in_array($lcm, ['getpassword', 'gettotpsecret', 'getplainpassword', 'getconfirmationtoken', 'gettotpauthenticationconfiguration'], true)) {
|
||||
throw new SecurityNotAllowedMethodError('Tried to access user secrets', User::class, $method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,7 @@ final class ReleaseVersion
|
||||
/**
|
||||
* Get all releases from GitHub.
|
||||
*
|
||||
* @throws \Exception
|
||||
* @return array|null
|
||||
* @return array<string, array{'version': string, 'date': \DateTimeInterface, 'url': string, 'download': string, 'content': string}>
|
||||
* @return array
|
||||
* @return array<string, array{'version': non-empty-string, 'date': \DateTimeInterface, 'url': non-empty-string, 'download': non-empty-string, 'content': string}>
|
||||
*/
|
||||
private function getReleasesFromGithub(): array
|
||||
{
|
||||
@@ -41,19 +38,24 @@ final class ReleaseVersion
|
||||
$context = stream_context_create($opts);
|
||||
|
||||
$releases = file_get_contents('https://api.github.com/repos/' . Constants::GITHUB_REPO . '/releases', false, $context);
|
||||
$releases = json_decode($releases);
|
||||
|
||||
if (!isset($releases[0])) {
|
||||
throw new \Exception('API error - no release found at GitHub repository: ' . Constants::GITHUB_REPO);
|
||||
if ($releases === false) {
|
||||
throw new \Exception('Could not load releases from GitHub repository: ' . Constants::GITHUB_REPO);
|
||||
}
|
||||
/** @var array<string, array{url: non-empty-string, html_url: non-empty-string, tag_name: non-empty-string, name: non-empty-string, draft: bool, immutable: bool, prerelease: bool, created_at: non-empty-string, updated_at: non-empty-string, published_at: non-empty-string, zipball_url: non-empty-string, body: string}> $releases */
|
||||
$releases = json_decode($releases, true);
|
||||
|
||||
if ($releases === false) {
|
||||
throw new \Exception('Failed parsing release found at GitHub repository: ' . Constants::GITHUB_REPO);
|
||||
}
|
||||
|
||||
$parsed = [];
|
||||
foreach ($releases as $release) {
|
||||
if ($release->draft || $release->prerelease) {
|
||||
if ($release['draft'] || $release['prerelease']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$normalized = $versionParser->normalize($release->tag_name);
|
||||
$normalized = $versionParser->normalize($release['tag_name']);
|
||||
} catch (\UnexpectedValueException $e) {
|
||||
continue;
|
||||
}
|
||||
@@ -62,19 +64,19 @@ final class ReleaseVersion
|
||||
continue;
|
||||
}
|
||||
|
||||
$date = $release->published_at;
|
||||
$date = $release['published_at'];
|
||||
try {
|
||||
$date = new \DateTimeImmutable($date);
|
||||
} catch (\Exception $ex) {
|
||||
// can be ignored, we return a string
|
||||
continue;
|
||||
}
|
||||
|
||||
$parsed[$normalized] = [
|
||||
'version' => $release->tag_name,
|
||||
'version' => $release['tag_name'],
|
||||
'date' => $date,
|
||||
'url' => $release->html_url,
|
||||
'download' => $release->zipball_url,
|
||||
'content' => $release->body,
|
||||
'url' => $release['html_url'],
|
||||
'download' => $release['zipball_url'],
|
||||
'content' => $release['body'],
|
||||
];
|
||||
}
|
||||
$versions = Semver::rsort(array_keys($parsed));
|
||||
|
||||
Reference in New Issue
Block a user