mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-20 17:15:51 +02:00
Added an "twig" lines mode, where the label lines are processed via a sandboxed twig.
This commit is contained in:
parent
c2db827c9d
commit
11225eb9cc
11 changed files with 548 additions and 7 deletions
|
@ -21,7 +21,7 @@ fos_ck_editor:
|
|||
height: 100
|
||||
enterMode: 2
|
||||
toolbar: label_toolbar
|
||||
extraPlugins: "partdb_label"
|
||||
extraPlugins: ["partdb_label", "showprotected"]
|
||||
font_names: >
|
||||
DejaVu Sans Mono/DejaVu Sans Mono;
|
||||
DejaVu Sans/DejaVu Sans;
|
||||
|
@ -43,6 +43,9 @@ fos_ck_editor:
|
|||
partdb_label:
|
||||
path: "ckeditor/plugins/partdb_label/"
|
||||
filename: "plugin.js"
|
||||
showprotected:
|
||||
path: "ckeditor/plugins/showprotected/"
|
||||
filename: "plugin.js"
|
||||
|
||||
|
||||
toolbars:
|
||||
|
|
|
@ -83,6 +83,10 @@ function findLabelForPlaceholder(search)
|
|||
}
|
||||
}
|
||||
|
||||
//Dont escape text inside of twig blocks
|
||||
CKEDITOR.config.protectedSource.push(/\{\{[\s\S]*?\}\}/g);
|
||||
CKEDITOR.config.protectedSource.push(/\{\%[\s\S]*?%\}/g);
|
||||
|
||||
CKEDITOR.plugins.add('partdb_label', {
|
||||
hidpi: true,
|
||||
icons: 'placeholder',
|
||||
|
|
52
public/ckeditor/plugins/showprotected/dialogs/protected.js
Normal file
52
public/ckeditor/plugins/showprotected/dialogs/protected.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
|
||||
CKEDITOR.dialog.add( 'showProtectedDialog', function( editor ) {
|
||||
|
||||
return {
|
||||
title: 'Edit Protected Source',
|
||||
minWidth: 300,
|
||||
minHeight: 60,
|
||||
onOk: function() {
|
||||
var newSourceValue = this.getContentElement( 'info', 'txtProtectedSource' ).getValue();
|
||||
|
||||
var encodedSourceValue = CKEDITOR.plugins.showprotected.encodeProtectedSource( newSourceValue );
|
||||
|
||||
this._.selectedElement.setAttribute('data-cke-realelement', encodedSourceValue);
|
||||
this._.selectedElement.setAttribute('title', newSourceValue);
|
||||
this._.selectedElement.setAttribute('alt', newSourceValue);
|
||||
},
|
||||
|
||||
onHide: function() {
|
||||
delete this._.selectedElement;
|
||||
},
|
||||
|
||||
onShow: function() {
|
||||
this._.selectedElement = editor.getSelection().getSelectedElement();
|
||||
|
||||
var decodedSourceValue = CKEDITOR.plugins.showprotected.decodeProtectedSource( this._.selectedElement.getAttribute('data-cke-realelement') );
|
||||
|
||||
this.setValueOf( 'info', 'txtProtectedSource', decodedSourceValue );
|
||||
},
|
||||
contents: [
|
||||
{
|
||||
id: 'info',
|
||||
label: 'Edit Protected Source',
|
||||
accessKey: 'I',
|
||||
elements: [
|
||||
{
|
||||
type: 'text',
|
||||
id: 'txtProtectedSource',
|
||||
label: 'Value',
|
||||
required: true,
|
||||
validate: function() {
|
||||
if ( !this.getValue() ) {
|
||||
alert( 'The value cannot be empty' );
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
} );
|
BIN
public/ckeditor/plugins/showprotected/images/code.gif
Normal file
BIN
public/ckeditor/plugins/showprotected/images/code.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 278 B |
105
public/ckeditor/plugins/showprotected/plugin.js
Normal file
105
public/ckeditor/plugins/showprotected/plugin.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* "showprotected" CKEditor plugin
|
||||
*
|
||||
* Created by Matthew Lieder (https://github.com/IGx89)
|
||||
*
|
||||
* Licensed under the MIT, GPL, LGPL and MPL licenses
|
||||
*
|
||||
* Icon courtesy of famfamfam: http://www.famfamfam.com/lab/icons/mini/
|
||||
*/
|
||||
|
||||
// TODO: configuration settings
|
||||
// TODO: show the actual text inline, not just an icon?
|
||||
// TODO: improve copy/paste behavior (tooltip is wrong after paste)
|
||||
|
||||
CKEDITOR.plugins.add( 'showprotected', {
|
||||
requires: 'dialog,fakeobjects',
|
||||
onLoad: function() {
|
||||
// Add the CSS styles for protected source placeholders.
|
||||
var iconPath = CKEDITOR.getUrl( this.path + 'images' + '/code.gif' ),
|
||||
baseStyle = 'background:url(' + iconPath + ') no-repeat %1 center;border:1px dotted #00f;background-size:16px;';
|
||||
|
||||
var template = '.%2 img.cke_protected' +
|
||||
'{' +
|
||||
baseStyle +
|
||||
'width:16px;' +
|
||||
'min-height:15px;' +
|
||||
// The default line-height on IE.
|
||||
'height:1.15em;' +
|
||||
// Opera works better with "middle" (even if not perfect)
|
||||
'vertical-align:' + ( CKEDITOR.env.opera ? 'middle' : 'text-bottom' ) + ';' +
|
||||
'}';
|
||||
|
||||
// Styles with contents direction awareness.
|
||||
function cssWithDir( dir ) {
|
||||
return template.replace( /%1/g, dir == 'rtl' ? 'right' : 'left' ).replace( /%2/g, 'cke_contents_' + dir );
|
||||
}
|
||||
|
||||
CKEDITOR.addCss( cssWithDir( 'ltr' ) + cssWithDir( 'rtl' ) );
|
||||
},
|
||||
|
||||
init: function( editor ) {
|
||||
CKEDITOR.dialog.add( 'showProtectedDialog', this.path + 'dialogs/protected.js' );
|
||||
|
||||
editor.on( 'doubleclick', function( evt ) {
|
||||
var element = evt.data.element;
|
||||
|
||||
if ( element.is( 'img' ) && element.hasClass( 'cke_protected' ) ) {
|
||||
evt.data.dialog = 'showProtectedDialog';
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
afterInit: function( editor ) {
|
||||
// Register a filter to displaying placeholders after mode change.
|
||||
|
||||
var dataProcessor = editor.dataProcessor,
|
||||
dataFilter = dataProcessor && dataProcessor.dataFilter;
|
||||
|
||||
if ( dataFilter ) {
|
||||
dataFilter.addRules( {
|
||||
comment: function( commentText, commentElement ) {
|
||||
if(commentText.indexOf(CKEDITOR.plugins.showprotected.protectedSourceMarker) == 0) {
|
||||
commentElement.attributes = [];
|
||||
var fakeElement = editor.createFakeParserElement( commentElement, 'cke_protected', 'protected' );
|
||||
|
||||
var cleanedCommentText = CKEDITOR.plugins.showprotected.decodeProtectedSource( commentText );
|
||||
fakeElement.attributes.title = fakeElement.attributes.alt = cleanedCommentText;
|
||||
|
||||
return fakeElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
/**
|
||||
* Set of showprotected plugin's helpers.
|
||||
*
|
||||
* @class
|
||||
* @singleton
|
||||
*/
|
||||
CKEDITOR.plugins.showprotected = {
|
||||
|
||||
protectedSourceMarker: '{cke_protected}',
|
||||
|
||||
decodeProtectedSource: function( protectedSource ) {
|
||||
if(protectedSource.indexOf('%3C!--') == 0) {
|
||||
return decodeURIComponent(protectedSource).replace( /<!--\{cke_protected\}([\s\S]+?)-->/g, function( match, data ) {
|
||||
return decodeURIComponent( data );
|
||||
} );
|
||||
} else {
|
||||
return decodeURIComponent(protectedSource.substr(CKEDITOR.plugins.showprotected.protectedSourceMarker.length));
|
||||
}
|
||||
},
|
||||
|
||||
encodeProtectedSource: function( protectedSource ) {
|
||||
return '<!--' + CKEDITOR.plugins.showprotected.protectedSourceMarker +
|
||||
encodeURIComponent( protectedSource ).replace( /--/g, '%2D%2D' ) +
|
||||
'-->';
|
||||
}
|
||||
|
||||
};
|
|
@ -26,6 +26,7 @@ use App\Entity\Contracts\NamedElementInterface;
|
|||
use App\Entity\LabelSystem\LabelOptions;
|
||||
use App\Entity\LabelSystem\LabelProfile;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Exceptions\TwigModeException;
|
||||
use App\Form\LabelOptionsType;
|
||||
use App\Form\LabelSystem\LabelDialogType;
|
||||
use App\Helpers\LabelResponse;
|
||||
|
@ -35,6 +36,7 @@ use App\Services\LabelSystem\LabelGenerator;
|
|||
use App\Services\Misc\RangeParser;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\SubmitButton;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
@ -53,7 +55,8 @@ class LabelController extends AbstractController
|
|||
protected $elementTypeNameGenerator;
|
||||
protected $rangeParser;
|
||||
|
||||
public function __construct(LabelGenerator $labelGenerator, EntityManagerInterface $em, ElementTypeNameGenerator $elementTypeNameGenerator, RangeParser $rangeParser)
|
||||
public function __construct(LabelGenerator $labelGenerator, EntityManagerInterface $em, ElementTypeNameGenerator $elementTypeNameGenerator,
|
||||
RangeParser $rangeParser)
|
||||
{
|
||||
$this->labelGenerator = $labelGenerator;
|
||||
$this->em = $em;
|
||||
|
@ -109,8 +112,13 @@ class LabelController extends AbstractController
|
|||
$target_id = (string) $form->get('target_id')->getData();
|
||||
$targets = $this->findObjects($form_options->getSupportedElement(), $target_id);
|
||||
if (!empty($targets)) {
|
||||
$pdf_data = $this->labelGenerator->generateLabel($form_options, $targets);
|
||||
$filename = $this->getLabelName($targets[0], $profile);
|
||||
try {
|
||||
$pdf_data = $this->labelGenerator->generateLabel($form_options, $targets);
|
||||
$filename = $this->getLabelName($targets[0], $profile);
|
||||
} catch (TwigModeException $exception) {
|
||||
$form->get('options')->get('lines')->addError(new FormError($exception->getMessage()));
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->addFlash('warning', 'label_generator.no_entities_found');
|
||||
}
|
||||
|
|
33
src/Exceptions/TwigModeException.php
Normal file
33
src/Exceptions/TwigModeException.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
|
||||
use Throwable;
|
||||
use Twig\Error\Error;
|
||||
|
||||
class TwigModeException extends \RuntimeException
|
||||
{
|
||||
public function __construct(Error $previous = null)
|
||||
{
|
||||
parent::__construct($previous->getMessage(), 0, $previous);
|
||||
}
|
||||
}
|
|
@ -105,6 +105,23 @@ class LabelOptionsType extends AbstractType
|
|||
],
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->add('lines_mode', ChoiceType::class, [
|
||||
'label' => 'label_options.lines_mode.label',
|
||||
'choices' => [
|
||||
'label_options.lines_mode.html' => 'html',
|
||||
'label.options.lines_mode.twig' => 'twig',
|
||||
],
|
||||
'help' => 'label_options.lines_mode.help',
|
||||
'help_html' => true,
|
||||
'expanded' => true,
|
||||
'attr' => [
|
||||
'class' => 'pt-2'
|
||||
],
|
||||
'label_attr' => [
|
||||
'class' => 'radio-custom radio-inline'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
|
|
|
@ -22,24 +22,34 @@ namespace App\Services\LabelSystem;
|
|||
|
||||
use App\Entity\Contracts\NamedElementInterface;
|
||||
use App\Entity\LabelSystem\LabelOptions;
|
||||
use App\Exceptions\TwigModeException;
|
||||
use App\Services\ElementTypeNameGenerator;
|
||||
use App\Services\LabelSystem\Barcodes\BarcodeContentGenerator;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Twig\Environment;
|
||||
use Twig\Error\Error;
|
||||
use Twig\Error\SyntaxError;
|
||||
|
||||
class LabelHTMLGenerator
|
||||
final class LabelHTMLGenerator
|
||||
{
|
||||
protected $twig;
|
||||
protected $elementTypeNameGenerator;
|
||||
protected $replacer;
|
||||
protected $barcodeGenerator;
|
||||
protected $sandboxedTwigProvider;
|
||||
protected $partdb_title;
|
||||
protected $security;
|
||||
|
||||
public function __construct(ElementTypeNameGenerator $elementTypeNameGenerator, LabelTextReplacer $replacer, Environment $twig,
|
||||
BarcodeGenerator $barcodeGenerator)
|
||||
BarcodeGenerator $barcodeGenerator, SandboxedTwigProvider $sandboxedTwigProvider, Security $security, string $partdb_title)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
$this->elementTypeNameGenerator = $elementTypeNameGenerator;
|
||||
$this->replacer = $replacer;
|
||||
$this->barcodeGenerator = $barcodeGenerator;
|
||||
$this->sandboxedTwigProvider = $sandboxedTwigProvider;
|
||||
$this->security = $security;
|
||||
$this->partdb_title = $partdb_title;
|
||||
}
|
||||
|
||||
public function getLabelHTML(LabelOptions $options, array $elements): string
|
||||
|
@ -49,15 +59,43 @@ class LabelHTMLGenerator
|
|||
}
|
||||
|
||||
$twig_elements = [];
|
||||
|
||||
if ($options->getLinesMode() === 'twig') {
|
||||
$sandboxed_twig = $this->sandboxedTwigProvider->getTwig($options);
|
||||
$current_user = $this->security->getUser();
|
||||
}
|
||||
|
||||
$page = 1;
|
||||
foreach ($elements as $element) {
|
||||
if ($options->getLinesMode() === 'twig' && $sandboxed_twig !== null) {
|
||||
try {
|
||||
$lines = $sandboxed_twig->render(
|
||||
'lines',
|
||||
[
|
||||
'element' => $element,
|
||||
'page' => $page,
|
||||
'user' => $current_user,
|
||||
'install_title' => $this->partdb_title,
|
||||
]
|
||||
);
|
||||
} catch (Error $exception) {
|
||||
throw new TwigModeException($exception);
|
||||
}
|
||||
} else {
|
||||
$lines = $this->replacer->replace($options->getLines(), $element);
|
||||
}
|
||||
|
||||
$twig_elements[] = [
|
||||
'element' => $element,
|
||||
'lines' => $this->replacer->replace($options->getLines(), $element),
|
||||
'lines' => $lines,
|
||||
'barcode' => $this->barcodeGenerator->generateSVG($options, $element),
|
||||
'barcode_content' => $this->barcodeGenerator->getContent($options, $element),
|
||||
];
|
||||
|
||||
$page++;
|
||||
}
|
||||
|
||||
|
||||
return $this->twig->render('LabelSystem/labels/base_label.html.twig', [
|
||||
'meta_title' => $this->getPDFTitle($options, $elements[0]),
|
||||
'elements' => $twig_elements,
|
||||
|
@ -65,6 +103,7 @@ class LabelHTMLGenerator
|
|||
]);
|
||||
}
|
||||
|
||||
|
||||
protected function getPDFTitle(LabelOptions $options, object $element)
|
||||
{
|
||||
if ($element instanceof NamedElementInterface) {
|
||||
|
|
140
src/Services/LabelSystem/SandboxedTwigProvider.php
Normal file
140
src/Services/LabelSystem/SandboxedTwigProvider.php
Normal file
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Services\LabelSystem;
|
||||
|
||||
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentContainingDBElement;
|
||||
use App\Entity\Base\AbstractCompany;
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
use App\Entity\Contracts\NamedElementInterface;
|
||||
use App\Entity\Contracts\TimeStampableInterface;
|
||||
use App\Entity\LabelSystem\LabelOptions;
|
||||
use App\Entity\Parameters\AbstractParameter;
|
||||
use App\Entity\Parts\MeasurementUnit;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\PartLot;
|
||||
use App\Entity\Parts\Storelocation;
|
||||
use App\Entity\Parts\Supplier;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Entity\PriceInformations\Orderdetail;
|
||||
use App\Entity\PriceInformations\Pricedetail;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Twig\AppExtension;
|
||||
use App\Twig\Sandbox\InheritanceSecurityPolicy;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
use Twig\Extra\Intl\IntlExtension;
|
||||
use Twig\Sandbox\SecurityPolicy;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
class SandboxedTwigProvider
|
||||
{
|
||||
protected const ALLOWED_TAGS = ['apply', 'autoescape', 'do', 'for', 'if', 'set', 'verbatim', 'with'];
|
||||
protected const ALLOWED_FILTERS = ['abs', 'batch', 'capitalize', 'column', 'country_name',
|
||||
'currency_name', 'currency_symbol', 'date', 'date_modify', 'default', 'escape', 'filter', 'first', 'format',
|
||||
'format_currency', 'format_date', 'format_datetime', 'format_number', 'format_time', 'join', 'keys',
|
||||
'language_name', 'last', 'length', 'locale_name', 'lower', 'map', 'merge', 'nl2br', 'raw', 'number_format',
|
||||
'reduce', 'replace', 'reverse', 'slice', 'sort', 'spaceless', 'split', 'striptags', 'timezone_name', 'title',
|
||||
'trim', 'upper', 'url_encode',
|
||||
//Part-DB specific filters:
|
||||
'moneyFormat', 'siFormat', 'amountFormat'];
|
||||
|
||||
protected const ALLOWED_FUNCTIONS = ['date', 'html_classes', 'max', 'min', 'random', 'range'];
|
||||
|
||||
protected const ALLOWED_METHODS = [
|
||||
NamedElementInterface::class => ['getName'],
|
||||
AbstractDBElement::class => ['getID', '__toString'],
|
||||
TimeStampableInterface::class => ['getLastModified', 'getAddedDate'],
|
||||
AbstractStructuralDBElement::class => ['isChildOf', 'isRoot', 'getParent', 'getComment', 'getLevel',
|
||||
'getFullPath', 'getPathArray', 'getChildren', 'isNotSelectable'],
|
||||
AbstractCompany::class => ['getAddress', 'getPhoneNumber', 'getFaxNumber', 'getEmailAddress', 'getWebsite'],
|
||||
AttachmentContainingDBElement::class => ['getAttachments', 'getMasterPictureAttachment'],
|
||||
Attachment::class => ['isPicture', 'is3DModel', 'isExternal', 'isSecure', 'isBuiltIn', 'getExtension',
|
||||
'getElement', 'getURL', 'getFilename', 'getAttachmentType', 'getShowInTable'],
|
||||
AbstractParameter::class => ['getFormattedValue', 'getGroup', 'getSymbol', 'getValueMin', 'getValueMax',
|
||||
'getValueTypical', 'getUnit', 'getValueText'],
|
||||
MeasurementUnit::class => ['getUnit', 'isInteger', 'useSIPrefix'],
|
||||
PartLot::class => ['isExpired', 'getDescription', 'getComment', 'getExpirationDate', 'getStorageLocation',
|
||||
'getPart', 'isInstockUnknown', 'getAmount', 'getNeedsRefill'],
|
||||
Storelocation::class => ['isFull', 'isOnlySinglePart', 'isLimitToExistingParts', 'getStorageType'],
|
||||
Supplier::class => ['getShippingCosts', 'getDefaultCurrency'],
|
||||
Part::class => ['isNeedsReview', 'getTags', 'getMass', 'getDescription', 'isFavorite', 'getCategory',
|
||||
'getFootprint', 'getPartLots', 'getPartUnit', 'useFloatAmount', 'getMinAmount', 'getAmountSum',
|
||||
'getManufacturerProductUrl', 'getCustomProductURL', 'getManufacturingStatus', 'getManufacturer',
|
||||
'getManufacturerProductNumber', 'getOrderdetails', 'isObsolete'],
|
||||
Currency::class => ['getIsoCode', 'getInverseExchangeRate', 'getExchangeRate'],
|
||||
Orderdetail::class => ['getPart', 'getSupplier', 'getSupplierPartNr', 'getObsolete',
|
||||
'getPricedetails', 'findPriceForQty', ],
|
||||
Pricedetail::class => ['getOrderdetail', 'getPrice', 'getPricePerUnit', 'getPriceRelatedQuantity',
|
||||
'getMinDiscountQuantity', 'getCurrency'],
|
||||
//Only allow very little information about users...
|
||||
User::class => ['isAnonymousUser', 'getUsername', 'getFullName', 'getFirstName', 'getLastName',
|
||||
'getDepartment', 'getEmail']
|
||||
|
||||
];
|
||||
protected const ALLOWED_PROPERTIES = [];
|
||||
|
||||
private $appExtension;
|
||||
|
||||
public function __construct(AppExtension $appExtension)
|
||||
{
|
||||
$this->appExtension = $appExtension;
|
||||
}
|
||||
|
||||
public function getTwig(LabelOptions $options): Environment
|
||||
{
|
||||
if ($options->getLinesMode() !== 'twig') {
|
||||
throw new \InvalidArgumentException('The LabelOptions must explicitly allow twig via lines_mode = "twig"!');
|
||||
}
|
||||
|
||||
|
||||
$loader = new \Twig\Loader\ArrayLoader([
|
||||
'lines' => $options->getLines(),
|
||||
]);
|
||||
$twig = new Environment($loader);
|
||||
|
||||
//Second argument activate sandbox globally.
|
||||
$sandbox = new SandboxExtension($this->getSecurityPolicy(), true);
|
||||
$twig->addExtension($sandbox);
|
||||
|
||||
//Add IntlExtension
|
||||
$twig->addExtension(new IntlExtension());
|
||||
|
||||
//Add Part-DB specific extension
|
||||
$twig->addExtension($this->appExtension);
|
||||
|
||||
return $twig;
|
||||
}
|
||||
|
||||
protected function getSecurityPolicy(): SecurityPolicyInterface
|
||||
{
|
||||
return new InheritanceSecurityPolicy(
|
||||
self::ALLOWED_TAGS,
|
||||
self::ALLOWED_FILTERS,
|
||||
self::ALLOWED_METHODS,
|
||||
self::ALLOWED_PROPERTIES,
|
||||
self::ALLOWED_FUNCTIONS
|
||||
);
|
||||
}
|
||||
}
|
140
src/Twig/Sandbox/InheritanceSecurityPolicy.php
Normal file
140
src/Twig/Sandbox/InheritanceSecurityPolicy.php
Normal file
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\Sandbox;
|
||||
|
||||
use Twig\Markup;
|
||||
use Twig\Sandbox\SecurityNotAllowedFilterError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFunctionError;
|
||||
use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
use Twig\Sandbox\SecurityNotAllowedPropertyError;
|
||||
use Twig\Sandbox\SecurityNotAllowedTagError;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
use Twig\Template;
|
||||
|
||||
/**
|
||||
* Represents a security policy which need to be enforced when sandbox mode is enabled.
|
||||
*
|
||||
* Modified by Jan Böhmer, to allow inheritance of methods and properties via class hierachy.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class InheritanceSecurityPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
private $allowedTags;
|
||||
private $allowedFilters;
|
||||
private $allowedMethods;
|
||||
private $allowedProperties;
|
||||
private $allowedFunctions;
|
||||
|
||||
public function __construct(array $allowedTags = [], array $allowedFilters = [], array $allowedMethods = [], array $allowedProperties = [], array $allowedFunctions = [])
|
||||
{
|
||||
$this->allowedTags = $allowedTags;
|
||||
$this->allowedFilters = $allowedFilters;
|
||||
$this->setAllowedMethods($allowedMethods);
|
||||
$this->allowedProperties = $allowedProperties;
|
||||
$this->allowedFunctions = $allowedFunctions;
|
||||
}
|
||||
|
||||
public function setAllowedTags(array $tags): void
|
||||
{
|
||||
$this->allowedTags = $tags;
|
||||
}
|
||||
|
||||
public function setAllowedFilters(array $filters): void
|
||||
{
|
||||
$this->allowedFilters = $filters;
|
||||
}
|
||||
|
||||
public function setAllowedMethods(array $methods): void
|
||||
{
|
||||
$this->allowedMethods = [];
|
||||
foreach ($methods as $class => $m) {
|
||||
$this->allowedMethods[$class] = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, \is_array($m) ? $m : [$m]);
|
||||
}
|
||||
}
|
||||
|
||||
public function setAllowedProperties(array $properties): void
|
||||
{
|
||||
$this->allowedProperties = $properties;
|
||||
}
|
||||
|
||||
public function setAllowedFunctions(array $functions): void
|
||||
{
|
||||
$this->allowedFunctions = $functions;
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
foreach ($tags as $tag) {
|
||||
if (!\in_array($tag, $this->allowedTags)) {
|
||||
throw new SecurityNotAllowedTagError(sprintf('Tag "%s" is not allowed.', $tag), $tag);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
if (!\in_array($filter, $this->allowedFilters)) {
|
||||
throw new SecurityNotAllowedFilterError(sprintf('Filter "%s" is not allowed.', $filter), $filter);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($functions as $function) {
|
||||
if (!\in_array($function, $this->allowedFunctions)) {
|
||||
throw new SecurityNotAllowedFunctionError(sprintf('Function "%s" is not allowed.', $function), $function);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
if ($obj instanceof Template || $obj instanceof Markup) {
|
||||
return;
|
||||
}
|
||||
|
||||
$allowed = false;
|
||||
$method = strtr($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
|
||||
foreach ($this->allowedMethods as $class => $methods) {
|
||||
if ($obj instanceof $class) {
|
||||
$allowed = \in_array($method, $methods);
|
||||
|
||||
//CHANGED: Only break if we the method is allowed, otherwise try it on the other methods
|
||||
if ($allowed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$allowed) {
|
||||
$class = \get_class($obj);
|
||||
throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$allowed = false;
|
||||
foreach ($this->allowedProperties as $class => $properties) {
|
||||
if ($obj instanceof $class) {
|
||||
$allowed = \in_array($property, \is_array($properties) ? $properties : [$properties]);
|
||||
|
||||
//CHANGED: Only break if we the method is allowed, otherwise try it on the other methods
|
||||
if ($allowed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$allowed) {
|
||||
$class = \get_class($obj);
|
||||
throw new SecurityNotAllowedPropertyError(sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue