diff --git a/README.md b/README.md new file mode 100644 index 0000000..05fb75d --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +Divante_ClassLocker +==================== +Divante Class Locker bundle was created to block modifications on attributes in classes. + +## Compatibility +This module is compatible with Pimcore >= 5.2 + +## Dependencies +pitpit/php-diff + +## Events +- pimcore.class.preUpdate + - Blocking any changes in class' attributes defined in config. + +## Module goal +The goal of this module is: +- blocking modifications in class' attributes defined in config. + +## Installing/Getting started +```bash +composer require divanteltd/pimcore-class-locker +``` +In Pimcore panel select Extensions and click Enable. + +Define which attributes cannot be changed in which class in your bundle's config.yml: +```yaml +divante_class_locker: + classes: + class_name1: + - attribute1 + - attribute2 + + class_name2: [attribute1, attribute2] +``` + +## Contributing + +If you'd like to contribute, please fork the repository and use a feature branch. Pull requests are warmly welcome. + +## Licensing + +GPL-3.0-or-later + +## Standards & Code Quality + +This module respects all Pimcore 5 code quality rules and our own PHPCS and PHPMD rulesets. + +## About Authors + +![Divante-logo](http://divante.co/logo-HG.png "Divante") + +We are a Software House from Europe, existing from 2008 and employing about 150 people. Our core competencies are built around Magento, Pimcore and bespoke software projects (we love Symfony3, Node.js, Angular, React, Vue.js). We specialize in sophisticated integration projects trying to connect hardcore IT with good product design and UX. + +We work for Clients like INTERSPORT, ING, Odlo, Onderdelenwinkel and CDP, the company that produced The Witcher game. We develop two projects: [Open Loyalty](http://www.openloyalty.io/ "Open Loyalty") - an open source loyalty program and [Vue.js Storefront](https://github.com/DivanteLtd/vue-storefront "Vue.js Storefront"). + +We are part of the OEX Group which is listed on the Warsaw Stock Exchange. Our annual revenue has been growing at a minimum of about 30% year on year. + +Visit our website [Divante.co](https://divante.co/ "Divante.co") for more information. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0014900 --- /dev/null +++ b/composer.json @@ -0,0 +1,44 @@ +{ + "name": "divanteltd/pimcore-class-locker", + "type": "pimcore-bundle", + "license": "GPL-3.0-or-later", + "description": "Divante Class Locker bundle was created to block modifications on attributes in classes.", + "keywords": [ + "pimcore", + "pimcore-bundle" + ], + "homepage": "https://www.divante.co", + "authors": [ + { + "name": "Michał Bolka", + "email": "mbolka@divante.pl", + "role": "Developer" + }, + { + "name": "Jakub Płaskonka", + "email": "jplaskonka@divante.pl", + "role": "Developer" + }, + { + "name": "Agata Drozdek", + "email": "adrozdek@divante.pl", + "role": "Developer" + } + ], + "require": { + "pimcore/pimcore": ">=5.2", + "pitpit/php-diff": "@dev" + }, + "autoload": { + "psr-4": { + "ClassLockerBundle\\": "src/" + } + }, + "extra": { + "pimcore": { + "bundles": [ + "ClassLockerBundle\\ClassLockerBundle" + ] + } + } +} diff --git a/src/ClassLockerBundle/DependencyInjection/Configuration.php b/src/ClassLockerBundle/DependencyInjection/Configuration.php new file mode 100644 index 0000000..9926f20 --- /dev/null +++ b/src/ClassLockerBundle/DependencyInjection/Configuration.php @@ -0,0 +1,41 @@ + | Agata Drozdek + * @copyright   Copyright (c) 2017 Divante Ltd. (https://divante.co) + */ + +namespace Divante\ClassLockerBundle\DependencyInjection; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; + +/** + * Class Configuration + * @package Divante\ClassLockerBundle\DependencyInjection + */ +class Configuration implements ConfigurationInterface +{ + /** + * {@inheritdoc} + */ + public function getConfigTreeBuilder() + { + $treeBuilder = new TreeBuilder(); + $rootNode = $treeBuilder->root('divante_class_locker'); + + $rootNode + ->children() + ->arrayNode('classes') + ->useAttributeAsKey('name') + ->prototype('array') + ->scalarPrototype()->end() + ->end() + ->end() + ->end() + ->end(); + + return $treeBuilder; + } +} diff --git a/src/ClassLockerBundle/DependencyInjection/DivanteClassLockerExtension.php b/src/ClassLockerBundle/DependencyInjection/DivanteClassLockerExtension.php new file mode 100644 index 0000000..a4a71bd --- /dev/null +++ b/src/ClassLockerBundle/DependencyInjection/DivanteClassLockerExtension.php @@ -0,0 +1,40 @@ + | Agata Drozdek + * @copyright   Copyright (c) 2017 Divante Ltd. (https://divante.co) + */ + +namespace Divante\ClassLockerBundle\DependencyInjection; + +use Divante\ClassLockerBundle\EventListener\ClassListener; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\DependencyInjection\Loader; + +/** + * This is the class that loads and manages your bundle configuration. + * + * @link http://symfony.com/doc/current/cookbook/bundles/extension.html + */ +class DivanteClassLockerExtension extends Extension +{ + /** + * {@inheritdoc} + * @param array $configs + * @param ContainerBuilder $container + */ + public function load(array $configs, ContainerBuilder $container) + { + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('services.yml'); + + $listener = $container->getDefinition(ClassListener::class); + $listener->setArgument('$lockedClasses', $config['classes']); + } +} diff --git a/src/ClassLockerBundle/DivanteClassLockerBundle.php b/src/ClassLockerBundle/DivanteClassLockerBundle.php new file mode 100644 index 0000000..f3f72c9 --- /dev/null +++ b/src/ClassLockerBundle/DivanteClassLockerBundle.php @@ -0,0 +1,45 @@ + + * @copyright   Copyright (c) 2017 Divante Ltd. (https://divante.co) + */ + +namespace Divante\ClassLockerBundle; + +use Pimcore\Extension\Bundle\AbstractPimcoreBundle; + +/** + * Class DivanteClassLockerBundle + * @package Divante\ClassLockerBundle + */ +class DivanteClassLockerBundle extends AbstractPimcoreBundle +{ + + /** + * @return array + */ + public function getJsPaths() + { + return [ + '/bundles/divanteclasslocker/js/pimcore/startup.js', + ]; + } + + /** + * @inheritdoc + */ + public function getNiceName() + { + return "Divante Class Locker"; + } + + /** + * @inheritdoc + */ + public function getVersion() + { + return "2.0.0"; + } +} diff --git a/src/ClassLockerBundle/EventListener/ClassListener.php b/src/ClassLockerBundle/EventListener/ClassListener.php new file mode 100644 index 0000000..edbbbfa --- /dev/null +++ b/src/ClassLockerBundle/EventListener/ClassListener.php @@ -0,0 +1,71 @@ + | Michal Bolka + * @copyright   Copyright (c) 2017 Divante Ltd. (https://divante.co) + */ + +namespace Divante\ClassLockerBundle\EventListener; + +use Divante\ClassLockerBundle\Service\ClassLockerService; +use Pimcore\Event\Model\DataObject\ClassDefinitionEvent; +use Pimcore\Model\DataObject\ClassDefinition; + +/** + * Class ClassListener + * @package Divante\ClassLockerBundle\EventListener + */ +class ClassListener +{ + /** @var ClassLockerService */ + protected $lockerService; + + /** + * @var array + */ + protected $lockedClasses; + + /** + * ClassListener constructor. + * @param ClassLockerService $lockerService + * @param array $lockedClasses + */ + public function __construct(ClassLockerService $lockerService, array $lockedClasses) + { + $this->lockerService = $lockerService; + $this->lockedClasses = $lockedClasses; + } + + /** + * @param ClassDefinitionEvent $classDefinitionEvent + * @throws \Exception + * @return void + */ + public function onPreUpdate(ClassDefinitionEvent $classDefinitionEvent): void + { + $classDefinition = $classDefinitionEvent->getClassDefinition(); + if (array_key_exists($classDefinition->getName(), $this->lockedClasses)) { + $this->preProcessTargetClass($classDefinition); + } + } + + /** + * @param ClassDefinition $targetClass + * @throws \Exception + * @return void + */ + protected function preProcessTargetClass(ClassDefinition $targetClass): void + { + $sharedFieldsNames = $this->lockedClasses[$targetClass->getName()] ?: []; + + $differences = $this->lockerService->fetchSharedAttributesChanges($targetClass); + $intersectedValues = array_intersect($sharedFieldsNames, $differences); + $count = count($intersectedValues); + + if ($count > 0) { + $glue = (php_sapi_name() == "cli") ? "\n" : "
"; + throw new \Exception("You cannot modify following fields:
" . implode($glue, $sharedFieldsNames)); + } + } +} diff --git a/src/ClassLockerBundle/Resources/config/services.yml b/src/ClassLockerBundle/Resources/config/services.yml new file mode 100644 index 0000000..c51a32d --- /dev/null +++ b/src/ClassLockerBundle/Resources/config/services.yml @@ -0,0 +1,11 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: true + + Divante\ClassLockerBundle\EventListener\ClassListener: + tags: + - { name: kernel.event_listener, event: pimcore.class.preUpdate, method: onPreUpdate } + + Divante\ClassLockerBundle\Service\ClassLockerService: ~ diff --git a/src/ClassLockerBundle/Resources/public/js/pimcore/startup.js b/src/ClassLockerBundle/Resources/public/js/pimcore/startup.js new file mode 100644 index 0000000..955ff6b --- /dev/null +++ b/src/ClassLockerBundle/Resources/public/js/pimcore/startup.js @@ -0,0 +1,46 @@ +pimcore.registerNS("pimcore.plugin.DivanteClassLockerBundle"); + +pimcore.plugin.DivanteClassLockerBundle = Class.create(pimcore.plugin.admin, { + getClassName: function () { + return "pimcore.plugin.DivanteClassLockerBundle"; + }, + + initialize: function () { + pimcore.plugin.broker.registerPlugin(this); + }, + + pimcoreReady: function (params, broker) { + //alert("DivanteClassLockerBundle ready!"); + } +}); + +var DivanteClassLockerBundlePlugin = new pimcore.plugin.DivanteClassLockerBundle(); +Ext.define('overrides.pimcore.object.classes.klass',{ + override:'pimcore.object.classes.klass', + saveOnComplete: function (response) { + + try { + var res = Ext.decode(response.responseText); + if(res.success) { + // refresh all class stores + this.parentPanel.tree.getStore().load(); + pimcore.globalmanager.get("object_types_store").load(); + pimcore.globalmanager.get("object_types_store_create").load(); + + // set the current modification date, to detect modifcations on the class which are not made here + this.data.modificationDate = res['class'].modificationDate; + + pimcore.helpers.showNotification(t("success"), t("class_saved_successfully"), "success"); + } else { + throw res.message; + } + } catch (e) { + this.saveOnError(e); + } + + }, + + saveOnError: function (e) { + pimcore.helpers.showNotification(t("error"), t(e), "error"); + } +}); \ No newline at end of file diff --git a/src/ClassLockerBundle/Service/ClassLockerService.php b/src/ClassLockerBundle/Service/ClassLockerService.php new file mode 100644 index 0000000..fd7b03a --- /dev/null +++ b/src/ClassLockerBundle/Service/ClassLockerService.php @@ -0,0 +1,104 @@ + + * @copyright   Copyright (c) 2017 Divante Ltd. (https://divante.co) + */ + +namespace Divante\ClassLockerBundle\Service; + +use Pimcore\Cache\Runtime; +use Pimcore\Model\DataObject\ClassDefinition; +use Pimcore\Model\DataObject\ClassDefinition\Data\Localizedfields; +use Pitpit\Component\Diff\Diff; +use Pitpit\Component\Diff\DiffEngine; + +/** + * Class Service + * + * @package Divante\ClassLockerBundle\Service + */ +class ClassLockerService +{ + + /** + * Creates an empty class if it does not exists already + * + * @param string $className + * @return ClassDefinition + */ + public function createEmptyClass(string $className) : ClassDefinition + { + $class = ClassDefinition::getByName($className); + if (!$class) { + $class = new ClassDefinition(); + $class->setName($className); + $class->save(); + } + + return $class; + } + + /** + * @param string $className + * @return array + */ + public function fetchFieldsFromClass(string $className) : array + { + $sharedDefinition = ClassDefinition::getByName($className); + $sharedFields = $sharedDefinition->getFieldDefinitions(); + return $this->fetchFieldsName($sharedFields); + } + + /** + * @param ClassDefinition\Data[] $fields + * @param array $attributes + * @return array + */ + public function fetchFieldsName(array $fields, &$attributes = []) : array + { + foreach ($fields as $values) { + if ($values instanceof ClassDefinition\Layout || $values instanceof Localizedfields) { + $this->fetchFieldsName($values->getChildren(), $attributes); + } else { + $attributes[] = $values->getName(); + } + } + return $attributes; + } + + /** + * @param ClassDefinition $targetClass + * @return array + */ + public function fetchSharedAttributesChanges(ClassDefinition $targetClass) : array + { + Runtime::clear(); + $originalDefinition = ClassDefinition::getByName($targetClass->getName()); + $originalFields = $originalDefinition->getFieldDefinitions(); + $engine = new DiffEngine(); + $diff = $engine->compare($originalFields, $targetClass->getFieldDefinitions()); + return $this->fetchModifiedAttributesNames($diff); + } + + /** + * @param Diff $diff + * @param array $differences + * @return array + */ + protected function fetchModifiedAttributesNames(Diff $diff, &$differences = []) : array + { + foreach ($diff as $element) { + if ($element->isModified() || $element->isDeleted()) { + if ($element->getOld() instanceof ClassDefinition\Data) { + $differences[] = $element->getOld()->getName(); + } + } + if ($diff->isModified()) { + $this->fetchModifiedAttributesNames($element, $differences); + } + } + return $differences; + } +}