├── .gitignore ├── tests ├── Codeception │ ├── Module │ │ ├── .gitkeep │ │ └── Config.php │ ├── Output │ │ └── .gitkeep │ ├── Page │ │ └── .gitkeep │ ├── Support │ │ ├── Data │ │ │ ├── dump.sql │ │ │ └── fixtures.php │ │ ├── _generated │ │ │ └── .gitignore │ │ ├── Helper │ │ │ └── Acceptance.php │ │ └── AcceptanceTester.php │ ├── Acceptance │ │ ├── _bootstrap.php │ │ ├── UsercentricsButtonCest.php │ │ ├── UsercentricsDeactivateBlockingCest.php │ │ ├── BaseCest.php │ │ ├── ScriptSnippetAdjustmentCest.php │ │ └── ScriptIncludeAdjustmentCest.php │ ├── Acceptance.suite.yml │ └── Config │ │ └── params.php ├── Integration │ ├── Service │ │ ├── ConfigTestData │ │ │ ├── EmptyTest.yaml │ │ │ ├── ConfigReadTest.yaml │ │ │ ├── Service1.yaml │ │ │ ├── ConfigPutTest.yaml │ │ │ └── Snippets.yaml │ │ ├── IntegrationScriptTest.php │ │ ├── ConfigTest.php │ │ ├── YamlStorageTest.php │ │ ├── RendererTest.php │ │ └── ScriptServiceMapperTest.php │ └── Core │ │ └── ViewConfigTest.php ├── PhpStan │ ├── phpstan.neon │ └── phpstan-bootstrap.php ├── codeception.yml ├── PhpMd │ └── standard.xml ├── Unit │ ├── Service │ │ ├── Integration │ │ │ ├── Pattern │ │ │ │ ├── CustomTest.php │ │ │ │ ├── CmpV1Test.php │ │ │ │ ├── CmpV2Test.php │ │ │ │ └── CmpV2TcfTest.php │ │ │ ├── IntegrationVersionFactoryTest.php │ │ │ └── IntegrationScriptBuilderTest.php │ │ └── ModuleSettingsTest.php │ ├── DataObjects │ │ ├── ServiceTest.php │ │ ├── ScriptTest.php │ │ ├── ScriptSnippetTest.php │ │ └── ConfigurationTest.php │ └── UnitTestCase.php ├── phpcs.xml └── phpunit.xml ├── assets └── logo.png ├── .gitmodules ├── src ├── Service │ ├── IntegrationScriptInterface.php │ ├── Integration │ │ ├── Pattern │ │ │ ├── IntegrationPatternInterface.php │ │ │ ├── CmpV2Legacy.php │ │ │ ├── CmpV2TcfLegacy.php │ │ │ ├── Custom.php │ │ │ ├── CmpV1.php │ │ │ ├── CmpV2.php │ │ │ └── CmpV2Tcf.php │ │ ├── IntegrationScriptBuilderInterface.php │ │ ├── IntegrationVersionFactoryInterface.php │ │ ├── IntegrationScriptBuilder.php │ │ └── IntegrationVersionFactory.php │ ├── ModuleSettingsInterface.php │ ├── Configuration │ │ ├── StorageInterface.php │ │ ├── ConfigurationDaoInterface.php │ │ ├── YamlStorage.php │ │ └── ConfigurationDao.php │ ├── IntegrationScript.php │ ├── ScriptServiceMapperInterface.php │ ├── RendererInterface.php │ ├── ModuleSettings.php │ ├── Renderer.php │ └── ScriptServiceMapper.php ├── Core │ ├── Module.php │ ├── ViewConfig.php │ └── ScriptRenderer.php ├── Exception │ ├── PatternNotFound.php │ └── WidgetsNotSupported.php ├── DataObject │ ├── Script.php │ ├── Service.php │ ├── ScriptSnippet.php │ └── Configuration.php └── Traits │ └── ServiceContainer.php ├── .github ├── pull_request_template.md ├── workflows │ ├── trigger.yaml │ ├── scheduled.yml │ └── dispatch_module.yml └── oxid-esales │ └── module-usercentrics.yaml ├── views ├── twig │ └── extensions │ │ └── themes │ │ └── default │ │ └── layout │ │ └── base.html.twig └── admin_twig │ ├── en │ └── module_options.php │ └── de │ └── module_options.php ├── services.yaml ├── recipes └── setup-development.sh ├── CONTRIBUTING.md ├── composer.json ├── metadata.php ├── README.md ├── CHANGELOG.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | _generated/* 2 | .idea -------------------------------------------------------------------------------- /tests/Codeception/Module/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/Codeception/Output/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/Codeception/Page/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/Codeception/Support/Data/dump.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/Codeception/Support/_generated/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/Integration/Service/ConfigTestData/EmptyTest.yaml: -------------------------------------------------------------------------------- 1 | scripts: 2 | services: 3 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OXID-eSales/usercentrics/HEAD/assets/logo.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "recipes/parts"] 2 | path = recipes/parts 3 | url = https://github.com/OXID-eSales/docker-eshop-sdk-recipe-parts.git 4 | -------------------------------------------------------------------------------- /tests/Integration/Service/ConfigTestData/ConfigReadTest.yaml: -------------------------------------------------------------------------------- 1 | scripts: 2 | - { service: TestService1Id, path: test1.js } 3 | services: 4 | - { name: name1, id: TestService1Id } 5 | -------------------------------------------------------------------------------- /src/Service/IntegrationScriptInterface.php: -------------------------------------------------------------------------------- 1 | $params 9 | */ 10 | public function getIntegrationScript(string $integrationVersion, array $params): string; 11 | } 12 | -------------------------------------------------------------------------------- /src/Service/Integration/IntegrationVersionFactoryInterface.php: -------------------------------------------------------------------------------- 1 | getShopRootPath(), 'source', 'bootstrap.php'); 13 | 14 | // This is acceptance bootstrap 15 | $helper = new \OxidEsales\Codeception\Module\FixturesHelper(); 16 | $helper->loadRuntimeFixtures(dirname(__FILE__) . '/../Support/Data/fixtures.php'); 17 | -------------------------------------------------------------------------------- /src/Service/Configuration/ConfigurationDaoInterface.php: -------------------------------------------------------------------------------- 1 | 3 | 4 | ## Description 5 | 6 | 7 | ## Types of changes 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | -------------------------------------------------------------------------------- /src/Service/Integration/Pattern/CmpV1.php: -------------------------------------------------------------------------------- 1 | scriptSource . '" 15 | id="{USERCENTRICS_CLIENT_ID}">'; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exception/PatternNotFound.php: -------------------------------------------------------------------------------- 1 | scriptSource . '" 16 | defer>'; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/DataObject/Script.php: -------------------------------------------------------------------------------- 1 | path; 22 | } 23 | 24 | public function getServiceId(): string 25 | { 26 | return $this->serviceId; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Service/Integration/Pattern/CmpV2Tcf.php: -------------------------------------------------------------------------------- 1 | scriptSource . '" 16 | data-tcf-enabled 17 | defer>'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/DataObject/Service.php: -------------------------------------------------------------------------------- 1 | name; 25 | } 26 | 27 | public function getId(): string 28 | { 29 | return $this->id; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DataObject/ScriptSnippet.php: -------------------------------------------------------------------------------- 1 | id; 25 | } 26 | 27 | public function getServiceId(): string 28 | { 29 | return $this->serviceId; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/PhpMd/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | Standard OXID Ruleset 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/Unit/Service/Integration/Pattern/CustomTest.php: -------------------------------------------------------------------------------- 1 | getIntegrationScriptPattern(); 22 | $this->assertSame('', $scriptPattern); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Codeception/Acceptance/UsercentricsButtonCest.php: -------------------------------------------------------------------------------- 1 | amOnPage($homePage->URL); 25 | 26 | $this->waitForUsercentrics($I); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Service/IntegrationScript.php: -------------------------------------------------------------------------------- 1 | moduleSettings->getUsercentricsId(); 18 | $mode = $this->moduleSettings->getUsercentricsMode(); 19 | 20 | $params = [ 21 | '{USERCENTRICS_CLIENT_ID}' => $clientId 22 | ]; 23 | 24 | return $this->scriptBuilder->getIntegrationScript($mode, $params); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Service/Integration/IntegrationScriptBuilder.php: -------------------------------------------------------------------------------- 1 | versionFactory->getPatternByVersion($integrationVersion); 14 | return $this->replacePatternParameters($patternConfig->getIntegrationScriptPattern(), $params); 15 | } 16 | 17 | /** 18 | * @param array $params 19 | */ 20 | private function replacePatternParameters(string $pattern, array $params): string 21 | { 22 | return strtr($pattern, $params); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Service/ScriptServiceMapperInterface.php: -------------------------------------------------------------------------------- 1 | getService(ModuleSettingsInterface::class); 18 | } 19 | 20 | public function getUsercentricsScript(): string 21 | { 22 | /** 23 | * @psalm-suppress InternalMethod 24 | * @var IntegrationScriptInterface $service 25 | */ 26 | $service = $this->getService(IntegrationScriptInterface::class); 27 | return $service->getIntegrationScript(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Unit/DataObjects/ServiceTest.php: -------------------------------------------------------------------------------- 1 | assertSame('testName', $service->getName()); 24 | } 25 | 26 | public function testHasId(): void 27 | { 28 | $service = new Service('testName', 'testId'); 29 | $this->assertSame('testId', $service->getId()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Service/RendererInterface.php: -------------------------------------------------------------------------------- 1 | > $pathGroups // [ 10 => ["test.js","test2.js"] ] 18 | * @param string $widget Widget name. 19 | * 20 | * @return string 21 | */ 22 | public function formFilesOutput(array $pathGroups, string $widget): string; 23 | 24 | /** 25 | * Encloses script code with a script tag 26 | * 27 | * @param string $scriptsOutput 28 | * @param string $widget 29 | * @param bool $isAjaxRequest 30 | * @return string 31 | */ 32 | public function encloseScriptSnippet(string $scriptsOutput, string $widget, bool $isAjaxRequest): string; 33 | } 34 | -------------------------------------------------------------------------------- /tests/Unit/DataObjects/ScriptTest.php: -------------------------------------------------------------------------------- 1 | assertSame('test/path', $service->getPath()); 24 | } 25 | 26 | public function testHasServiceId(): void 27 | { 28 | $service = new Script('test/path', 'testServiceId'); 29 | $this->assertSame('testServiceId', $service->getServiceId()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Traits/ServiceContainer.php: -------------------------------------------------------------------------------- 1 | $serviceName 25 | * 26 | * @return T 27 | */ 28 | protected function getServiceFromContainer(string $serviceName) 29 | { 30 | return ContainerFactory::getInstance() 31 | ->getContainer() 32 | ->get($serviceName); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Unit/DataObjects/ScriptSnippetTest.php: -------------------------------------------------------------------------------- 1 | assertSame('123ABC', $service->getId()); 24 | } 25 | 26 | public function testHasServiceId(): void 27 | { 28 | $service = new ScriptSnippet('123ABC', 'testServiceId'); 29 | $this->assertSame('testServiceId', $service->getServiceId()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Unit/Service/Integration/Pattern/CmpV1Test.php: -------------------------------------------------------------------------------- 1 | getIntegrationScriptPattern(); 22 | $this->assertHtmlEquals( 23 | '', 26 | $scriptPattern 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/trigger.yaml: -------------------------------------------------------------------------------- 1 | name: Auto trigger on push or pull requests 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - 'b-7.4.x**' 8 | 9 | jobs: 10 | php83_mysql80: 11 | uses: oxid-eSales/github-actions/.github/workflows/universal_workflow_light.yaml@v5 12 | with: 13 | testplan: '~/defaults/7.4.x.yaml,~/defaults/php8.3_mysql8.0_only.yaml,~/module-usercentrics.yaml,~/_custom.yaml' 14 | runs_on: '"ubuntu-latest"' 15 | defaults: 'v5' 16 | plan_folder: '.github/oxid-esales' 17 | custom_testplan_yaml: | 18 | finish: 19 | slack_title: 'PHP83-MySQL80-{{ .Github.EventName }}-{{ .Github.RefName }}' 20 | secrets: 21 | DOCKER_HUB_USER: ${{ secrets.DOCKER_HUB_USER }} 22 | DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }} 23 | CACHE_ENDPOINT: ${{ secrets.CACHE_ENDPOINT }} 24 | CACHE_ACCESS_KEY: ${{ secrets.CACHE_ACCESS_KEY }} 25 | CACHE_SECRET_KEY: ${{ secrets.CACHE_SECRET_KEY }} 26 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 27 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 28 | -------------------------------------------------------------------------------- /tests/Unit/Service/Integration/Pattern/CmpV2Test.php: -------------------------------------------------------------------------------- 1 | getIntegrationScriptPattern(); 22 | $this->assertHtmlEquals( 23 | '', 27 | $scriptPattern 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/scheduled.yml: -------------------------------------------------------------------------------- 1 | name: Scheduled weekly run 2 | 3 | on: 4 | schedule: 5 | # This cron job runs at 1:16 AM every 7th day of the month, every month of the year. 6 | - cron: '16 1 */7 * *' 7 | 8 | jobs: 9 | matrix_74x_weekly: 10 | uses: oxid-eSales/github-actions/.github/workflows/universal_workflow_light.yaml@v5 11 | with: 12 | testplan: '~/defaults/7.4.x.yaml,~/defaults/scheduled.yaml,~/module-usercentrics.yaml,~/_custom.yaml' 13 | runs_on: '"ubuntu-latest"' 14 | defaults: 'v5' 15 | plan_folder: '.github/oxid-esales' 16 | custom_testplan_yaml: | 17 | finish: 18 | slack_title: 'Matrix-74x-Weekly-{{ .Github.EventName }}-{{ .Github.RefName }}' 19 | secrets: 20 | DOCKER_HUB_USER: ${{ secrets.DOCKER_HUB_USER }} 21 | DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }} 22 | CACHE_ENDPOINT: ${{ secrets.CACHE_ENDPOINT }} 23 | CACHE_ACCESS_KEY: ${{ secrets.CACHE_ACCESS_KEY }} 24 | CACHE_SECRET_KEY: ${{ secrets.CACHE_SECRET_KEY }} 25 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 26 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 27 | -------------------------------------------------------------------------------- /tests/Unit/Service/Integration/Pattern/CmpV2TcfTest.php: -------------------------------------------------------------------------------- 1 | getIntegrationScriptPattern(); 22 | $this->assertHtmlEquals( 23 | '', 28 | $scriptPattern 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DataObject/Configuration.php: -------------------------------------------------------------------------------- 1 | scripts; 33 | } 34 | 35 | /** 36 | * @return Service[] 37 | */ 38 | public function getServices(): array 39 | { 40 | return $this->services; 41 | } 42 | 43 | /** 44 | * @return ScriptSnippet[] 45 | */ 46 | public function getScriptSnippets(): array 47 | { 48 | return $this->scriptSnippets; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Service/Integration/IntegrationVersionFactory.php: -------------------------------------------------------------------------------- 1 | Pattern\CmpV1::class, 12 | Pattern\CmpV2::VERSION_NAME => Pattern\CmpV2::class, 13 | Pattern\CmpV2Legacy::VERSION_NAME => Pattern\CmpV2Legacy::class, 14 | Pattern\CmpV2Tcf::VERSION_NAME => Pattern\CmpV2Tcf::class, 15 | Pattern\CmpV2TcfLegacy::VERSION_NAME => Pattern\CmpV2TcfLegacy::class, 16 | Pattern\Custom::VERSION_NAME => Pattern\Custom::class 17 | ]; 18 | 19 | /** 20 | * @throws PatternNotFound 21 | */ 22 | public function getPatternByVersion(string $integrationVersion): IntegrationPatternInterface 23 | { 24 | if (!isset(self::VERSION_MAP[$integrationVersion])) { 25 | throw new PatternNotFound(); 26 | } 27 | 28 | return new (self::VERSION_MAP[$integrationVersion]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Oxid Coding Standard 4 | 5 | 6 | ../src/ 7 | ../tests/ 8 | ./ 9 | 13 | 14 | ./tests/Codeception/Config 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ./ 34 | 35 | 36 | 37 | 38 | ./ 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | 26 | Unit/ 27 | 28 | 29 | Integration/ 30 | 31 | 32 | 33 | 34 | ../src 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/Unit/Service/Integration/IntegrationVersionFactoryTest.php: -------------------------------------------------------------------------------- 1 | getPatternByVersion($versionName); 25 | 26 | $this->assertInstanceOf(CmpV1::class, $patternObject); 27 | } 28 | 29 | public function testGetNotExistingIntegrationScriptPattern(): void 30 | { 31 | $this->expectException(PatternNotFound::class); 32 | 33 | $versionName = 'NotExisting'; 34 | $factory = new IntegrationVersionFactory(); 35 | $factory->getPatternByVersion($versionName); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Service/Configuration/YamlStorage.php: -------------------------------------------------------------------------------- 1 | dumper = new Dumper(); 23 | $this->parser = new Parser(); 24 | } 25 | 26 | 27 | public function getData(): array 28 | { 29 | if (file_exists($this->getConfigurationFilePath())) { 30 | /** @var mixed $result */ 31 | $result = $this->parser->parseFile($this->getConfigurationFilePath()); 32 | } 33 | 34 | if (!isset($result) || !is_array($result)) { 35 | $result = []; 36 | } 37 | 38 | return $result; 39 | } 40 | 41 | public function putData(array $data): void 42 | { 43 | $yamlData = $this->dumper->dump($data, 2); 44 | 45 | file_put_contents($this->getConfigurationFilePath(), $yamlData); 46 | } 47 | 48 | private function getConfigurationFilePath(): string 49 | { 50 | return $this->directory . DIRECTORY_SEPARATOR . $this->fileName; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Codeception/Acceptance/UsercentricsDeactivateBlockingCest.php: -------------------------------------------------------------------------------- 1 | setSmartDataProtectorActive(true); 23 | $I->setSmartDataProtectorDeactivateBlocking('xxx , yyy'); 24 | } 25 | 26 | public function _after(AcceptanceTester $I, Config $configModule): void 27 | { 28 | parent::_after($I, $configModule); 29 | 30 | $I->setSmartDataProtectorActive(false); 31 | $I->setSmartDataProtectorDeactivateBlocking(''); 32 | } 33 | 34 | /** 35 | * @throws \Exception 36 | * @group usercentrics 37 | */ 38 | public function protectorBlockingDeactivationConfigurationPresent(AcceptanceTester $I): void 39 | { 40 | $homePage = new Home($I); 41 | $I->amOnPage($homePage->URL); 42 | 43 | $I->seeInSource(''); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Unit/UnitTestCase.php: -------------------------------------------------------------------------------- 1 | loadHTML($expected, LIBXML_HTML_NOIMPLIED); 43 | $eXml = $eDom->saveXML(); 44 | 45 | $aDom = new DOMDocument(); 46 | $aDom->loadHTML($actual, LIBXML_HTML_NOIMPLIED); 47 | $aXml = $aDom->saveXML(); 48 | 49 | $this->assertXmlStringEqualsXmlString($eXml, $aXml); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Integration/Service/IntegrationScriptTest.php: -------------------------------------------------------------------------------- 1 | createMock(IntegrationScriptBuilderInterface::class); 24 | $scriptBuilder 25 | ->expects($this->once()) 26 | ->method('getIntegrationScript') 27 | ->with('usercentrics_mode', ['{USERCENTRICS_CLIENT_ID}' => 'usercentrics_id']) 28 | ->willReturn('integration script'); 29 | 30 | $settings = $this->createMock(ModuleSettingsInterface::class); 31 | $settings->method('getUsercentricsId')->willReturn('usercentrics_id'); 32 | $settings->method('getUsercentricsMode')->willReturn('usercentrics_mode'); 33 | 34 | $integrationScript = new IntegrationScript($scriptBuilder, $settings); 35 | $this->assertEquals('integration script', $integrationScript->getIntegrationScript()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Codeception/Module/Config.php: -------------------------------------------------------------------------------- 1 | '', 24 | 'config_file' => '' 25 | ]; 26 | 27 | public function _depends(): array 28 | { 29 | return []; 30 | } 31 | 32 | private function getConfigManager(): ConfigurationDao 33 | { 34 | $storage = new YamlStorage( 35 | $this->config['shop_path'], 36 | $this->config['config_file'] 37 | ); 38 | 39 | return new ConfigurationDao($storage); 40 | } 41 | 42 | public function getConfiguration(): Configuration 43 | { 44 | $configManager = $this->getConfigManager(); 45 | return $configManager->getConfiguration(); 46 | } 47 | 48 | public function putConfiguration(Configuration $configuration): void 49 | { 50 | $configManager = $this->getConfigManager(); 51 | $configManager->putConfiguration($configuration); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /views/twig/extensions/themes/default/layout/base.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html.twig" %} 2 | 3 | {% set settings = oViewConf.getUsercentricsModuleSettings %} 4 | 5 | {% block base_js %} 6 | 7 | {% if settings.getUsercentricsId %} 8 | {{ oViewConf.getUsercentricsScript()|raw }} 9 | {% endif %} 10 | {{ parent() }} 11 | 12 | {% endblock %} 13 | 14 | 15 | {% block head_meta_description %} 16 | 17 | {{ parent() }} 18 | 19 | 20 | {% if settings.getUsercentricsId and settings.isSmartProtectorEnabled %} 21 | 22 | 23 | 24 | {% set deactivateBlocking = settings.getSmartProtectorBlockingDisabledServices %} 25 | {% if deactivateBlocking %} 26 | 27 | {% endif %} 28 | {% endif %} 29 | 30 | {% if settings.getUsercentricsId and settings.isDevelopmentAutomaticConsentActive %} 31 | 43 | {% endif %} 44 | 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | _defaults: 4 | public: false 5 | autowire: true 6 | 7 | OxidProfessionalServices\Usercentrics\Service\Configuration\ConfigurationDaoInterface: 8 | class: OxidProfessionalServices\Usercentrics\Service\Configuration\ConfigurationDao 9 | arguments: 10 | $storage: '@OxidProfessionalServices\Usercentrics\Service\Configuration\StorageInterface' 11 | 12 | OxidProfessionalServices\Usercentrics\Service\Configuration\StorageInterface: 13 | class: OxidProfessionalServices\Usercentrics\Service\Configuration\YamlStorage 14 | public: true 15 | arguments: 16 | $directory: !php/const INSTALLATION_ROOT_PATH 17 | $fileName: /var/configuration/usercentrics.yaml 18 | 19 | OxidProfessionalServices\Usercentrics\Service\ScriptServiceMapperInterface: 20 | class: OxidProfessionalServices\Usercentrics\Service\ScriptServiceMapper 21 | 22 | OxidProfessionalServices\Usercentrics\Service\RendererInterface: 23 | class: OxidProfessionalServices\Usercentrics\Service\Renderer 24 | public: true 25 | 26 | OxidProfessionalServices\Usercentrics\Service\IntegrationScriptInterface: 27 | class: OxidProfessionalServices\Usercentrics\Service\IntegrationScript 28 | public: true 29 | 30 | OxidProfessionalServices\Usercentrics\Service\ModuleSettingsInterface: 31 | class: OxidProfessionalServices\Usercentrics\Service\ModuleSettings 32 | public: true 33 | 34 | OxidProfessionalServices\Usercentrics\Service\Integration\IntegrationScriptBuilderInterface: 35 | class: OxidProfessionalServices\Usercentrics\Service\Integration\IntegrationScriptBuilder 36 | public: true 37 | 38 | OxidProfessionalServices\Usercentrics\Service\Integration\IntegrationVersionFactoryInterface: 39 | class: OxidProfessionalServices\Usercentrics\Service\Integration\IntegrationVersionFactory 40 | -------------------------------------------------------------------------------- /tests/Codeception/Acceptance/BaseCest.php: -------------------------------------------------------------------------------- 1 | configBackup = $configModule->getConfiguration(); 23 | $this->prepareConfiguration($configModule); 24 | 25 | $I->setUsercentricsId('3j0TmWxNS'); 26 | $I->setUsercentricsMode('CmpV2'); 27 | $I->setDevelopmentAutomaticConsent(false); 28 | 29 | $I->clearShopCache(); 30 | } 31 | 32 | public function _after(AcceptanceTester $I, Config $configModule): void 33 | { 34 | $configModule->putConfiguration($this->configBackup); 35 | 36 | $I->setUsercentricsId(''); 37 | } 38 | 39 | /** 40 | * Prepare some test configuration before tests 41 | */ 42 | protected function prepareConfiguration(Config $configModule): void 43 | { 44 | } 45 | 46 | protected function waitForUsercentrics(AcceptanceTester $I, bool $accept = false): void 47 | { 48 | $I->waitForElement("#usercentrics-root", 10); 49 | $I->waitForJS("return typeof UC_UI !== 'undefined' && UC_UI !== null && UC_UI.isInitialized()"); 50 | 51 | if ($accept) { 52 | $I->executeJS("UC_UI.acceptAllConsents() && UC_UI.restartCMP()"); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Integration/Core/ViewConfigTest.php: -------------------------------------------------------------------------------- 1 | createStub(ModuleSettingsInterface::class); 24 | 25 | $viewConfig = $this->createPartialMock( 26 | get_class(oxNew(ViewConfig::class)), 27 | ['getService'] 28 | ); 29 | $viewConfig 30 | ->method('getService') 31 | ->with(ModuleSettingsInterface::class) 32 | ->willReturn($settingsStub); 33 | 34 | $this->assertSame($settingsStub, $viewConfig->getUsercentricsModuleSettings()); 35 | } 36 | 37 | public function testGetUsercentricsScript(): void 38 | { 39 | $script = $this->createMock(IntegrationScriptInterface::class); 40 | $script 41 | ->method('getIntegrationScript') 42 | ->willReturn('script content'); 43 | 44 | $viewConfig = $this->createPartialMock( 45 | get_class(oxNew(ViewConfig::class)), 46 | ['getService'] 47 | ); 48 | $viewConfig 49 | ->method('getService') 50 | ->with(IntegrationScriptInterface::class) 51 | ->willReturn($script); 52 | 53 | $this->assertEquals('script content', $viewConfig->getUsercentricsScript()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/oxid-esales/module-usercentrics.yaml: -------------------------------------------------------------------------------- 1 | install: 2 | cache: 3 | prepared_shop: false 4 | composer: 5 | root_url: '' 6 | transform: | 7 | { 8 | "require": { 9 | "oxid-esales/oxideshop-ce": "{{ .Data.install.composer.dev_ref }}", 10 | "oxid-esales/twig-admin-theme": "{{ .Data.install.composer.dev_ref }}", 11 | "oxid-esales/twig-component": "{{ .Data.install.composer.dev_ref }}", 12 | "oxid-esales/apex-theme": "{{ .Data.install.composer.dev_ref }}" 13 | } 14 | } 15 | custom_script_container: | 16 | vendor/bin/oe-console oe:database:reset --db-host=mysql --db-port=3306 --db-name=example --db-user=root --db-password=root --force 17 | vendor/bin/oe-console oe:module:install ./ 18 | 19 | vendor/bin/oe-eshop-doctrine_migration migrations:migrate 20 | 21 | vendor/bin/oe-console oe:module:activate oxps_usercentrics 22 | vendor/bin/oe-console oe:theme:activate apex 23 | 24 | runscript: &runscript 25 | matrix: 26 | script: | 27 | [ 28 | "report:tests-unit", 29 | "report:tests-integration", 30 | "no_report:tests-codeception", 31 | ] 32 | report: 33 | path: '' 34 | no_report: 35 | path: '' 36 | coverage_prefix: '' 37 | 38 | runslim: 39 | <<: *runscript 40 | matrix: 41 | script: | 42 | [ 43 | "report:phpstan-report", 44 | "no_report:phpmd-report", 45 | "no_report:phpcs-report", 46 | ] 47 | 48 | sonarcloud: 49 | matrix: 50 | testplan: '["-"]' 51 | strip_path: '/var/www/' 52 | organization: 'oxid-esales' 53 | project_name: 'oxid-professional-services/usercentrics' 54 | project_key: 'OXID-eSales_usercentrics' 55 | target_branch: 'b-7.4.x' 56 | parameters: > 57 | -Dsonar.language=php 58 | -Dsonar.scm.provider=git 59 | -Dsonar.sources=src 60 | -Dsonar.tests=tests 61 | 62 | finish: 63 | slack_title: 'Module UserCentrics ({{ .Data.install.git.ref }}) by {{ .Github.Actor }}' -------------------------------------------------------------------------------- /views/admin_twig/en/module_options.php: -------------------------------------------------------------------------------- 1 | "UTF-8", 12 | 'SHOP_MODULE_GROUP_usercentrics_main' => 'Usercentrics Integration', 13 | 'SHOP_MODULE_GROUP_usercentrics_advanced' => 'Advanced settings', 14 | 'SHOP_MODULE_smartDataProtectorActive' => 'Activate Usercentrics Smart Data Protector', 15 | 'HELP_SHOP_MODULE_smartDataProtectorActive' => 'Disabling this function could have legal consequences.', 16 | 'SHOP_MODULE_smartDataProtectorDeactivateBlocking' => 'List of services to Disable blocking', 17 | 'HELP_SHOP_MODULE_smartDataProtectorDeactivateBlocking' => 'More information: here', 18 | 'SHOP_MODULE_usercentricsId' => 'Usercentrics Script ID', 19 | 'SHOP_MODULE_usercentricsMode' => 'Usercentrics Mode and Version', 20 | 'SHOP_MODULE_usercentricsMode_CmpV1' => 'CMP Version 1', 21 | 'SHOP_MODULE_usercentricsMode_CmpV2' => 'CMP Version 2', 22 | 'SHOP_MODULE_usercentricsMode_CmpV2Legacy' => 'CMP V2 for old Browsers', 23 | 'SHOP_MODULE_usercentricsMode_CmpV2Tcf' => 'TCF 2.0', 24 | 'SHOP_MODULE_usercentricsMode_CmpV2TcfLegacy' => 'TCF 2.0 for old Browsers', 25 | 'SHOP_MODULE_usercentricsMode_Custom' => 'Include usercentrics script by yourself', 26 | 'HELP_SHOP_MODULE_usercentricsMode' => 'Please choose the mode for Usercentrics! A documentation about versions, legacy browser support and TCF support can be found at docs.usercentrics.com. Choose "Include usercentrics script by yourself" if the module itself should not add the usercentrics script.', 27 | ]; 28 | -------------------------------------------------------------------------------- /tests/Unit/DataObjects/ConfigurationTest.php: -------------------------------------------------------------------------------- 1 | getServices(); 32 | $this->assertCount(1, $services); 33 | $this->assertSame($service, $services[0]); 34 | } 35 | 36 | public function testHasScript(): void 37 | { 38 | $script = new Script('path', 'id'); 39 | $configuration = new Configuration( 40 | [], 41 | [$script], 42 | [] 43 | ); 44 | $scripts = $configuration->getScripts(); 45 | $this->assertCount(1, $scripts); 46 | $this->assertSame($script, $scripts[0]); 47 | } 48 | 49 | public function testHasScriptSnippets(): void 50 | { 51 | $scriptSnippet = new ScriptSnippet('123', 'id'); 52 | $configuration = new Configuration( 53 | [], 54 | [], 55 | [$scriptSnippet] 56 | ); 57 | $scripts = $configuration->getScriptSnippets(); 58 | $this->assertCount(1, $scripts); 59 | $this->assertSame($scriptSnippet, $scripts[0]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /views/admin_twig/de/module_options.php: -------------------------------------------------------------------------------- 1 | "UTF-8", 12 | 'SHOP_MODULE_GROUP_usercentrics_main' => 'Usercentrics Integration', 13 | 'SHOP_MODULE_GROUP_usercentrics_advanced' => 'Erweiterte Einstellungen', 14 | 'SHOP_MODULE_smartDataProtectorActive' => 'Smart Data Protector aktivieren', 15 | 'HELP_SHOP_MODULE_smartDataProtectorActive' => 'Die Deaktivierung dieser Funktion könnte rechtliche Folgen haben.', 16 | 'SHOP_MODULE_smartDataProtectorDeactivateBlocking' => 'Liste von Services, für welche der Smart Data Protector deaktiviert ist', 17 | 'HELP_SHOP_MODULE_smartDataProtectorDeactivateBlocking' => 'Weitere Informationen: hier', 18 | 'SHOP_MODULE_usercentricsId' => 'Usercentrics Script ID', 19 | 'SHOP_MODULE_usercentricsMode' => 'Usercentrics Modus und Version', 20 | 'SHOP_MODULE_usercentricsMode_CmpV1' => 'CMP Version 1', 21 | 'SHOP_MODULE_usercentricsMode_CmpV2' => 'CMP Version 2', 22 | 'SHOP_MODULE_usercentricsMode_CmpV2Legacy' => 'CMP V2 für alte Browser', 23 | 'SHOP_MODULE_usercentricsMode_CmpV2Tcf' => 'TCF 2.0', 24 | 'SHOP_MODULE_usercentricsMode_CmpV2TcfLegacy' => 'TCF 2.0 für alte Browser', 25 | 'SHOP_MODULE_usercentricsMode_Custom' => 'Usercentrics Script selbst einbinden', 26 | 'HELP_SHOP_MODULE_usercentricsMode' => 'Bitte wählen Sie hier den Modus aus, in dem Usercentrics läuft! Eine Erklärung der Versionen, Legacy Support und TCF Support finden Sie unter docs.usercentrics.com. Nutzen Sie die Option „Usercentrics Script selbst einbinden“ nur, wenn das Modul das Usercentrics Script nicht hinzufügen soll.', 27 | ]; 28 | -------------------------------------------------------------------------------- /tests/Codeception/Acceptance/ScriptSnippetAdjustmentCest.php: -------------------------------------------------------------------------------- 1 | updateConfigInDatabase('iNewBasketItemMessage', 2, 'str'); 27 | $basket = new Basket($I); 28 | 29 | $I->openShop(); 30 | $basket->addProductToBasket('1000', 1); 31 | 32 | $value = $I->grabAttributeFrom("//script[@data-oxid][1]", "data-oxid"); 33 | $this->prepareSpecialConfiguration($configModule, $value); 34 | 35 | $I->openShop(); 36 | $basket->addProductToBasket('1000', 1); 37 | 38 | $I->waitForElement("//script[@data-oxid='$value'][@data-usercentrics='testcustomservice'][@type='text/plain']"); 39 | $this->waitForUsercentrics($I, true); // Accept cookie policy 40 | $I->waitForElement("//script[@data-oxid='$value'][@type='text/javascript']"); 41 | } 42 | 43 | /** 44 | * Prepare some test configuration before tests 45 | */ 46 | protected function prepareSpecialConfiguration(Config $configModule, string $value): void 47 | { 48 | $config = new Configuration( 49 | [ //services 50 | new Service('testcustomservice', 'testcustomservice') 51 | ], 52 | [], 53 | [ //snippets 54 | new ScriptSnippet($value, 'testcustomservice') 55 | ] 56 | ); 57 | $configModule->putConfiguration($config); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Codeception/Acceptance.suite.yml: -------------------------------------------------------------------------------- 1 | # suite config 2 | actor: AcceptanceTester 3 | path: Acceptance 4 | bootstrap: _bootstrap.php 5 | modules: 6 | enabled: 7 | - Asserts 8 | - WebDriver: 9 | url: '%SHOP_URL%' 10 | host: '%SELENIUM_SERVER_IP%' 11 | browser: '%BROWSER_NAME%' 12 | port: '%SELENIUM_SERVER_PORT%' 13 | window_size: 1920x1080 14 | clear_cookies: true 15 | - \OxidEsales\Codeception\Module\ShopSetup: 16 | dump: '%DUMP_PATH%' 17 | fixtures: '%FIXTURES_PATH%' 18 | license: '%license_key%' 19 | - Db: 20 | dsn: 'mysql:host=%DB_HOST%;dbname=%DB_NAME%;charset=utf8' 21 | user: '%DB_USERNAME%' 22 | password: '%DB_PASSWORD%' 23 | port: '%DB_PORT%' 24 | dump: '%DUMP_PATH%' 25 | mysql_config: '%MYSQL_CONFIG_PATH%' 26 | populate: true # run populator before all tests 27 | cleanup: true # run populator before each test 28 | populator: 'mysql --defaults-file=$mysql_config --default-character-set=utf8 $dbname < $dump' 29 | initial_queries: 30 | - 'SET @@SESSION.sql_mode=""' 31 | - \OxidEsales\Codeception\Module\Oxideshop: 32 | screen_shot_url: '%SCREEN_SHOT_URL%' 33 | depends: 34 | - WebDriver 35 | - Db 36 | - \OxidEsales\Codeception\Module\OxideshopAdmin: 37 | depends: 38 | - WebDriver 39 | - \OxidEsales\Codeception\Module\Oxideshop 40 | - \OxidEsales\Codeception\Module\Database: 41 | config_key: 'fq45QS09_fqyx09239QQ' 42 | depends: Db 43 | - \OxidEsales\Codeception\Module\SelectTheme: 44 | theme_id: '%THEME_ID%' 45 | depends: 46 | - \OxidEsales\Codeception\Module\Database 47 | - \OxidEsales\Codeception\Module\Translation\TranslationsModule: 48 | shop_path: '%SHOP_SOURCE_PATH%' 49 | paths: 50 | - 'Application/views/apex' 51 | - '%SOURCE_RELATIVE_PACKAGE_PATH%/translations' 52 | - '%SOURCE_RELATIVE_PACKAGE_PATH%/views/admin_twig' 53 | - \OxidProfessionalServices\Usercentrics\Tests\Codeception\Module\Config: 54 | shop_path: '%SHOP_SOURCE_PATH%' 55 | config_file: '../var/configuration/usercentrics.yaml' 56 | - \OxidEsales\Codeception\Module\OxideshopModules: -------------------------------------------------------------------------------- /tests/Codeception/Acceptance/ScriptIncludeAdjustmentCest.php: -------------------------------------------------------------------------------- 1 | amOnPage($homePage->URL); 28 | 29 | $I->seeElementInDOM("//script[@type='text/plain'][@data-usercentrics='testcustomservice']"); 30 | $this->waitForUsercentrics($I, false); 31 | 32 | $I->seeElementInDOM("//script[@type='text/plain'][@data-usercentrics='testcustomservice']"); 33 | } 34 | 35 | /** 36 | * @group usercentrics 37 | */ 38 | public function scriptIncludeDecoratedAccepted(AcceptanceTester $I): void 39 | { 40 | $homePage = new Home($I); 41 | $I->amOnPage($homePage->URL); 42 | 43 | $I->seeElementInDOM("//script[@type='text/plain'][@data-usercentrics='testcustomservice']"); 44 | $this->waitForUsercentrics($I, true); 45 | 46 | $I->dontSeeElementInDOM("//script[@type='text/plain'][@data-usercentrics='testcustomservice']"); 47 | } 48 | 49 | /** 50 | * Prepare some test configuration before tests 51 | */ 52 | protected function prepareConfiguration(Config $configModule): void 53 | { 54 | $config = new Configuration( 55 | [ //services 56 | new Service('testcustomservice', 'testcustomservice') 57 | ], 58 | [ //scripts 59 | new Script('.min.js', 'testcustomservice') 60 | ], 61 | [] 62 | ); 63 | $configModule->putConfiguration($config); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Core/ScriptRenderer.php: -------------------------------------------------------------------------------- 1 | getServiceFromContainer(RendererInterface::class); 36 | return $service->encloseScriptSnippet($scriptsOutput, $widget, $isAjaxRequest); 37 | } catch (WidgetsNotSupported | ServiceNotFoundException) { 38 | return parent::enclose($scriptsOutput, $widget, $isAjaxRequest); 39 | } 40 | } 41 | 42 | /** 43 | * Form output for includes. 44 | * 45 | * @todo: remove ServiceNotFoundException from catch when service availability during module activation fixed 46 | * 47 | * @param array> $includes String files to include. 48 | * @param string $widget Widget name. 49 | * 50 | * @return string 51 | * 52 | * @psalm-suppress MoreSpecificImplementedParamType 53 | */ 54 | protected function formFilesOutput($includes, $widget) 55 | { 56 | try { 57 | $service = $this->getServiceFromContainer(RendererInterface::class); 58 | return $service->formFilesOutput($includes, $widget); 59 | } catch (WidgetsNotSupported | ServiceNotFoundException) { 60 | return parent::formFilesOutput($includes, $widget); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Service/ModuleSettings.php: -------------------------------------------------------------------------------- 1 | getStringSettingValue(self::USERCENTRICS_ID); 32 | } 33 | 34 | public function getUsercentricsMode(): string 35 | { 36 | return $this->getStringSettingValue(self::USERCENTRICS_MODE); 37 | } 38 | 39 | public function isSmartProtectorEnabled(): bool 40 | { 41 | return $this->moduleSettingService 42 | ->getBoolean(self::USERCENTRICS_SMART_DATA_PROTECTOR_ENABLED, Module::MODULE_ID); 43 | } 44 | 45 | public function getSmartProtectorBlockingDisabledServices(): array 46 | { 47 | $string = $this->getStringSettingValue(self::USERCENTRICS_SMART_DATA_PROTECTOR_BLOCKING_DISABLED); 48 | 49 | return array_map(function ($element) { 50 | return trim($element); 51 | }, explode(",", $string)); 52 | } 53 | 54 | public function isDevelopmentAutoConsentEnabled(): bool 55 | { 56 | return $this->moduleSettingService 57 | ->getBoolean(self::USERCENTRICS_DEVELOPMENT_AUTO_CONSENT, Module::MODULE_ID); 58 | } 59 | 60 | private function getStringSettingValue(string $key): string 61 | { 62 | return $this->moduleSettingService 63 | ->getString($key, Module::MODULE_ID) 64 | ->trim() 65 | ->toString(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Integration/Service/ConfigTest.php: -------------------------------------------------------------------------------- 1 | getVirtualStructurePath(); 28 | $file = 'ConfigPutTest.yaml'; 29 | 30 | $sut = new ConfigurationDao($this->getStorage($file, $directory)); 31 | 32 | $services = [new Service('name', 'TestServiceId')]; 33 | $scripts = [new Script('test.js', 'TestServiceId')]; 34 | $snippets = [new ScriptSnippet('123', 'TestServiceId')]; 35 | $configuration = new Configuration($services, $scripts, $snippets); 36 | 37 | $sut->putConfiguration($configuration); 38 | 39 | $this->assertFileEquals( 40 | __DIR__ . '/ConfigTestData/ConfigPutTest.yaml', 41 | $directory . DIRECTORY_SEPARATOR . $file 42 | ); 43 | } 44 | 45 | public function testConfigGet(): void 46 | { 47 | $file = 'ConfigReadTest.yaml'; 48 | 49 | $sut = new ConfigurationDao($this->getStorage($file, __DIR__ . '/ConfigTestData/')); 50 | 51 | $configuration = $sut->getConfiguration(); 52 | 53 | $oneService = $configuration->getServices()['TestService1Id']; 54 | $this->assertEquals("TestService1Id", $oneService->getId()); 55 | $this->assertEquals("name1", $oneService->getName()); 56 | 57 | $oneScript = $configuration->getScripts()[0]; 58 | $this->assertEquals("test1.js", $oneScript->getPath()); 59 | $this->assertEquals("TestService1Id", $oneScript->getServiceId()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Integration/Service/YamlStorageTest.php: -------------------------------------------------------------------------------- 1 | getContainer(); 25 | /** @var StorageInterface $storage */ 26 | $storage = $container->get(StorageInterface::class); 27 | 28 | $this->assertInstanceOf(YamlStorage::class, $storage); 29 | } 30 | 31 | public function testGetData(): void 32 | { 33 | $path = $this->getVirtualStructurePath([ 34 | 'ConfigReadTest.yaml' => 'test: value' 35 | ]); 36 | 37 | $sut = new YamlStorage( 38 | $path, 39 | 'ConfigReadTest.yaml' 40 | ); 41 | 42 | $this->assertEquals(["test" => "value"], $sut->getData()); 43 | } 44 | 45 | public function testGetNotExistingFileDataGivesEmptyArray(): void 46 | { 47 | $path = $this->getVirtualStructurePath([]); 48 | $file = 'ConfigReadTest.yaml'; 49 | 50 | $sut = new YamlStorage( 51 | $path, 52 | 'ConfigReadTest.yaml' 53 | ); 54 | 55 | $this->assertFileDoesNotExist($path . DIRECTORY_SEPARATOR . $file); 56 | 57 | $this->assertEquals([], $sut->getData()); 58 | } 59 | 60 | public function testPutData(): void 61 | { 62 | $path = $this->getVirtualStructurePath([ 63 | 'ConfigReadTest.yaml' => 'test: wrongValue' 64 | ]); 65 | 66 | $sut = new YamlStorage( 67 | $path, 68 | 'ConfigReadTest.yaml' 69 | ); 70 | 71 | $sut->putData(["test" => "correctValue"]); 72 | 73 | $this->assertEquals(["test" => "correctValue"], $sut->getData()); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Codeception/Config/params.php: -------------------------------------------------------------------------------- 1 | $facts->getShopUrl(), 24 | 'SHOP_SOURCE_PATH' => $facts->getSourcePath(), 25 | 'SOURCE_RELATIVE_PACKAGE_PATH' => getSourceRelativePackagePath($facts), 26 | 'VENDOR_PATH' => $facts->getVendorPath(), 27 | 'DB_NAME' => $facts->getDatabaseName(), 28 | 'DB_USERNAME' => $facts->getDatabaseUserName(), 29 | 'DB_PASSWORD' => $facts->getDatabasePassword(), 30 | 'DB_HOST' => $facts->getDatabaseHost(), 31 | 'DB_PORT' => $facts->getDatabasePort(), 32 | 'DUMP_PATH' => getTestDataDumpFilePath(), 33 | 'FIXTURES_PATH' => getTestFixtureSqlFilePath(), 34 | 'MYSQL_CONFIG_PATH' => getMysqlConfigPath(), 35 | 'SELENIUM_SERVER_PORT' => getenv('SELENIUM_SERVER_PORT') ?: '4444', 36 | 'SELENIUM_SERVER_IP' => getenv('SELENIUM_SERVER_IP') ?: 'selenium', 37 | 'THEME_ID' => getenv('THEME_ID') ?: 'twig', 38 | 'BROWSER_NAME' => getenv('BROWSER_NAME') ?: 'chrome', 39 | 'PHP_BIN' => $phpBinEnv, 40 | 'SCREEN_SHOT_URL' => $screenShotPathEnv 41 | ]; 42 | 43 | function getSourceRelativePackagePath(Facts $facts): string 44 | { 45 | return str_replace($facts->getShopRootPath(), '..', __DIR__) . '/../../../'; 46 | } 47 | 48 | function getTestDataDumpFilePath(): string 49 | { 50 | return getShopTestPath() . '/Codeception/Support/Data/generated/shop-dump.sql'; 51 | } 52 | 53 | function getTestFixtureSqlFilePath(): string 54 | { 55 | return getShopTestPath() . '/Codeception/Support/Data/dump.sql'; 56 | } 57 | 58 | function getShopTestPath(): string 59 | { 60 | $facts = new Facts(); 61 | 62 | if ($facts->isEnterprise()) { 63 | $shopTestPath = $facts->getEnterpriseEditionRootPath() . '/Tests'; 64 | } else { 65 | $shopTestPath = $facts->getCommunityEditionRootPath() . '/tests'; 66 | } 67 | 68 | return $shopTestPath; 69 | } 70 | 71 | function getMysqlConfigPath(): string 72 | { 73 | $configFile = new ConfigFile(); 74 | 75 | return (new DatabaseDefaultsFileGenerator($configFile))->generate(); 76 | } -------------------------------------------------------------------------------- /.github/workflows/dispatch_module.yml: -------------------------------------------------------------------------------- 1 | name: Manual trigger 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | limit: 7 | type: choice 8 | options: 9 | - 'no' 10 | - 'PHP8.2/MySQL5.7' 11 | - 'PHP8.2/MySQL8.0' 12 | - 'PHP8.2/MariaDb11' 13 | - 'PHP8.3/MySQL5.7' 14 | - 'PHP8.3/MySQL8.0' 15 | - 'PHP8.3/MySQL8.4' 16 | - 'PHP8.3/MariaDb11' 17 | - 'PHP8.4/MySQL8.0' 18 | - 'PHP8.4/MySQL8.4' 19 | - 'PHP8.4/MariaDb11' 20 | default: 'PHP8.3/MySQL8.0' 21 | description: 'Limit to one PHP/MySQL combination' 22 | 23 | jobs: 24 | build_testplan: 25 | runs-on: ubuntu-latest 26 | outputs: 27 | testplan: '${{ steps.build_testplan.outputs.testplan }}' 28 | steps: 29 | - name: 'Build testplan' 30 | id: build_testplan 31 | run: | 32 | # Build testplan 33 | # shellcheck disable=SC2088 34 | case "${{ inputs.limit }}" in 35 | "no") LIMIT='';; 36 | "PHP8.2/MySQL5.7") LIMIT='~/defaults/php8.2_mysql5.7_only.yaml,' ;; 37 | "PHP8.2/MySQL8.0") LIMIT='~/defaults/php8.2_mysql8.0_only.yaml,' ;; 38 | "PHP8.2/MariaDb11") LIMIT='~/defaults/php8.2_mariadb11_only.yaml,' ;; 39 | "PHP8.3/MySQL5.7") LIMIT='~/defaults/php8.3_mysql5.7_only.yaml,' ;; 40 | "PHP8.3/MySQL8.0") LIMIT='~/defaults/php8.3_mysql8.0_only.yaml,' ;; 41 | "PHP8.3/MySQL8.4") LIMIT='~/defaults/php8.3_mysql8.4_only.yaml,' ;; 42 | "PHP8.3/MariaDb11") LIMIT='~/defaults/php8.3_mariadb11_only.yaml,' ;; 43 | "PHP8.4/MySQL8.0") LIMIT='~/defaults/php8.4_mysql8.0_only.yaml,' ;; 44 | "PHP8.4/MySQL8.4") LIMIT='~/defaults/php8.4_mysql8.4_only.yaml,' ;; 45 | "PHP8.4/MariaDb11") LIMIT='~/defaults/php8.4_mariadb11_only.yaml,' ;; 46 | *) echo "Illegal choice, fix the workflow" 47 | exit 1 48 | ;; 49 | esac 50 | # shellcheck disable=SC2088 51 | TESTPLAN="~/defaults/7.4.x.yaml,${LIMIT}~/module-usercentrics.yaml" 52 | echo "testplan=${TESTPLAN}" | tee -a "${GITHUB_OUTPUT}" 53 | 54 | dispatch_stable: 55 | needs: build_testplan 56 | uses: oxid-eSales/github-actions/.github/workflows/universal_workflow_light.yaml@v5 57 | with: 58 | testplan: ${{ needs.build_testplan.outputs.testplan }} 59 | runs_on: '"ubuntu-latest"' 60 | defaults: 'v5' 61 | plan_folder: '.github/oxid-esales' 62 | secrets: 63 | DOCKER_HUB_USER: ${{ secrets.DOCKER_HUB_USER }} 64 | DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }} 65 | CACHE_ENDPOINT: ${{ secrets.CACHE_ENDPOINT }} 66 | CACHE_ACCESS_KEY: ${{ secrets.CACHE_ACCESS_KEY }} 67 | CACHE_SECRET_KEY: ${{ secrets.CACHE_SECRET_KEY }} 68 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 69 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 70 | -------------------------------------------------------------------------------- /recipes/setup-development.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Flags possible: 3 | # -e for shop edition. Possible values: CE/EE 4 | 5 | edition='EE' 6 | while getopts e: flag; do 7 | case "${flag}" in 8 | e) edition=${OPTARG} ;; 9 | *) ;; 10 | esac 11 | done 12 | 13 | SCRIPT_PATH=$(dirname ${BASH_SOURCE[0]}) 14 | 15 | cd $SCRIPT_PATH/../../ || exit 16 | 17 | # Prepare services configuration 18 | make setup 19 | make addbasicservices 20 | make file=services/adminer.yml addservice 21 | make file=services/selenium-chrome.yml addservice 22 | make file=services/node.yml addservice 23 | 24 | # Configure containers 25 | perl -pi\ 26 | -e 's#error_reporting = .*#error_reporting = E_ALL ^ E_WARNING ^ E_DEPRECATED#g;'\ 27 | containers/php/custom.ini 28 | 29 | perl -pi\ 30 | -e 's#/var/www/#/var/www/source/#g;'\ 31 | containers/httpd/project.conf 32 | 33 | perl -pi\ 34 | -e 's#PHP_VERSION=.*#PHP_VERSION=8.2#g;'\ 35 | .env 36 | 37 | docker compose up --build -d php 38 | 39 | docker compose exec -T php git config --global --add safe.directory /var/www 40 | 41 | $SCRIPT_PATH/parts/shared/require_shop_edition_packages.sh -e"${edition}" -v"dev-b-7.4.x" 42 | $SCRIPT_PATH/parts/shared/require_twig_components.sh -e"${edition}" -b"b-7.4.x" 43 | $SCRIPT_PATH/parts/shared/require.sh -n"oxid-esales/developer-tools" -v"dev-b-7.4.x" 44 | $SCRIPT_PATH/parts/shared/require.sh -n"oxid-esales/oxideshop-doctrine-migration-wrapper" -v"dev-b-7.4.x" 45 | $SCRIPT_PATH/parts/shared/require_demodata_package.sh -e"${edition}" -b"b-7.4.x" 46 | $SCRIPT_PATH/parts/shared/require_theme.sh -t"apex" -b"b-7.4.x" 47 | 48 | docker compose exec -T -w /var/www php \ 49 | composer config allow-plugins.oxid-esales/oxideshop-composer-plugin true 50 | 51 | docker compose exec -T php composer update --no-interaction 52 | 53 | make up 54 | 55 | $SCRIPT_PATH/parts/shared/setup_database.sh --no-demodata 56 | 57 | docker compose exec -T php vendor/bin/oe-console oe:module:install ./ 58 | docker compose exec -T php vendor/bin/oe-eshop-doctrine_migration migrations:migrate 59 | docker compose exec -T php vendor/bin/oe-eshop-db_views_generate 60 | docker compose exec -T php vendor/bin/oe-console oe:module:activate oxps_usercentrics 61 | docker compose exec -T php vendor/bin/oe-console oe:theme:activate apex 62 | 63 | $SCRIPT_PATH/parts/shared/create_admin.sh 64 | 65 | # Register all related project packages git repositories 66 | mkdir -p .idea; mkdir -p source/.idea; cp "${SCRIPT_PATH}/parts/bases/vcs.xml.base" .idea/vcs.xml 67 | perl -pi\ 68 | -e 's##\n #g;'\ 69 | -e 's##\n #g;'\ 70 | -e 's##\n #g;'\ 71 | -e 's##\n #g;'\ 72 | .idea/vcs.xml -------------------------------------------------------------------------------- /tests/Codeception/Support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | saveConfStringVar('usercentricsId', $usercentricsId); 39 | } 40 | 41 | public function setUsercentricsMode(string $mode): void 42 | { 43 | $this->saveConfStringVar('usercentricsMode', $mode); 44 | } 45 | 46 | public function setSmartDataProtectorDeactivateBlocking(string $value): void 47 | { 48 | $this->saveConfStringVar('smartDataProtectorDeactivateBlocking', $value); 49 | } 50 | 51 | private function saveConfStringVar(string $sVarName, string $sVarVal): void 52 | { 53 | $moduleSettingsService = $this->getServiceFromContainer(ModuleSettingServiceInterface::class); 54 | $moduleSettingsService->saveString($sVarName, $sVarVal, Module::MODULE_ID); 55 | } 56 | 57 | public function setDevelopmentAutomaticConsent(bool $value): void 58 | { 59 | $this->saveConfBoolVar('developmentAutomaticConsent', $value); 60 | } 61 | 62 | public function setSmartDataProtectorActive(bool $value): void 63 | { 64 | $this->saveConfBoolVar('smartDataProtectorActive', $value); 65 | } 66 | 67 | private function saveConfBoolVar(string $sVarName, bool $sVarVal): void 68 | { 69 | $moduleSettingsService = $this->getServiceFromContainer(ModuleSettingServiceInterface::class); 70 | $moduleSettingsService->saveBoolean($sVarName, $sVarVal, Module::MODULE_ID); 71 | } 72 | 73 | /** 74 | * Open shop first page. 75 | */ 76 | public function openShop(): Home 77 | { 78 | $I = $this; 79 | $homePage = new Home($I); 80 | $I->amOnPage($homePage->URL); 81 | 82 | return $homePage; 83 | } 84 | 85 | public function waitForPageLoad(int $timeout = 60): void 86 | { 87 | if (getenv('THEME_ID') !== 'apex') { 88 | $this->getScenario()->runStep(new Action('waitForPageLoad', func_get_args())); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the OXID eSales Usercentrics Module 2 | 3 | First off: Thank you for taking the time to contribute! 👍 4 | 5 | ## Contributor License Agreement 6 | 7 | When contributing code for the first time, you must sign our [Contributor License Agreement](https://gist.github.com/OXID-Admin/6df6ed126d074a54507d). No need to do something now, CLA Assistant will contact you after opening your first pull request. You can find more information about it on the [OXID Contribution and Contributor Agreement FAQ](https://docs.oxid-esales.com/developer/en/latest/development/modules_components_themes/contribution.html). 8 | 9 | ## How Can I Contribute? 10 | 11 | ### Reporting Bugs 12 | 13 | So, you stumbled upon a 🐛, those are tracked on the [Oxid Bugtracker](https://bugs.oxid-esales.com/) (module Usercentrics section). Create an issue and provide all necessary information by explaining the problem and include additional details to help maintainers reproduce the problem: 14 | 15 | - **Use a clear and descriptive title** for the issue to identify the problem 16 | - **Describe the exact steps which reproduce the problem** in as many details as possible 17 | - **Describe the behavior you observed** 18 | - **Explain which behavior you expected to see instead and why** 19 | - **State the shop and module version** you used while having the problem 20 | 21 | ### Suggesting Enhancements 22 | 23 | 📈 Enhancement suggestions are tracked on the [Oxid Bugtracker](https://bugs.oxid-esales.com/) (module Usercentrics section). Create an issue and provide all necessary information to help maintainers: 24 | 25 | - **Use a clear and descriptive title** for the issue to identify the suggestion 26 | - **Describe the current behavior** and **explain which behavior you expected to see instead** 27 | - **Explain why this enhancement would be useful** 28 | - **State if you are willing to provide a pull request for the enhancement** 29 | 30 | ### Pull Requests 31 | 32 | The basic workflow and the only way of contributing code to the project is via GitHub Pull Requests. 33 | 34 | - **Fork the project** to your account 35 | - **Make your bug fix or enhancement** 36 | - **Add tests** for your new code 37 | - **Send the Pull Request** 38 | - Make sure **CI is 💚** 39 | 40 | We recommend you to create a topic branch to work on, this will come in handy when making multiple contributions or when you need to rebase your pull request onto another branch. Also we would like to encourage you to open up an issue before making your pull request. 41 | 42 | #### What branch to send a Pull Request to? 43 | 44 | - Backwards compatibility breaks must go to the `master` branch 45 | - New Features go to the latest major version branch 46 | - Bugfixes to the latest minor version branch 47 | 48 | If in doubt, open up an issue before writing code, so we can help you settle this and possible other questions. 49 | 50 | ## I Need Help! 51 | 52 | When you need technical help on Git and GitHub, the [GitHub Help Page](https://help.github.com/) has you covered. You may additionally find [other sources with good practices](http://codeinthehole.com/writing/pull-requests-and-other-good-practices-for-teams-using-github/). 53 | -------------------------------------------------------------------------------- /src/Service/Renderer.php: -------------------------------------------------------------------------------- 1 | > $pathGroups // [ 10 => ["test.js","test2.js"] ] 22 | * 23 | * @throws WidgetsNotSupported 24 | */ 25 | public function formFilesOutput(array $pathGroups, string $widget): string 26 | { 27 | if ($widget) { 28 | throw new WidgetsNotSupported(); 29 | } 30 | 31 | if (!count($pathGroups)) { 32 | return ''; 33 | } 34 | 35 | ksort($pathGroups); // Sort by priority. 36 | 37 | /** @var string[] $sources */ 38 | $sources = []; 39 | foreach ($pathGroups as $priorityGroup) { 40 | /** @var string $onePath */ 41 | foreach ($priorityGroup as $onePath) { 42 | if (!in_array($onePath, $sources)) { 43 | $sources[] = (string)$onePath; 44 | } 45 | } 46 | } 47 | 48 | return $this->prepareScriptUrlsOutput($sources); 49 | } 50 | 51 | /** 52 | * @param array $sources //[ "test.js","test2.js"] 53 | * 54 | * see https://usercentrics.com/knowledge-hub/direct-integration-usercentrics-script-website/#Assign_data_attributes 55 | */ 56 | protected function prepareScriptUrlsOutput(array $sources): string 57 | { 58 | $outputs = []; 59 | 60 | foreach ($sources as $source) { 61 | $data = ''; 62 | $type = ' type="text/javascript"'; 63 | $src = ' src="' . $source . '"'; 64 | 65 | $service = $this->scriptServiceMapper->getServiceByScriptUrl($source); 66 | if ($service !== null) { 67 | $type = ' type="text/plain"'; 68 | $data = ' data-usercentrics="' . $service->getName() . '"'; 69 | } 70 | $outputs[] = ""; 71 | } 72 | 73 | return implode(PHP_EOL, $outputs); 74 | } 75 | 76 | /** 77 | * @throws WidgetsNotSupported 78 | */ 79 | public function encloseScriptSnippet(string $scriptsOutput, string $widget, bool $isAjaxRequest): string 80 | { 81 | if ($widget && !$isAjaxRequest) { 82 | throw new WidgetsNotSupported(); 83 | } 84 | 85 | if ($scriptsOutput) { 86 | $snippetId = $this->scriptServiceMapper->calculateSnippetId($scriptsOutput); 87 | $service = $this->scriptServiceMapper->getServiceBySnippetId($snippetId); 88 | 89 | $serviceData = $service ? ' data-usercentrics="' . $service->getName() . '"' : ''; 90 | $scriptType = $service ? ' type="text/plain"' : ' type="text/javascript"'; 91 | 92 | $snippetIdData = " data-oxid=\"$snippetId\""; 93 | 94 | return "$scriptsOutput"; 95 | } 96 | return ""; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Unit/Service/Integration/IntegrationScriptBuilderTest.php: -------------------------------------------------------------------------------- 1 | 'ABC123']; 21 | 22 | /** 23 | * @dataProvider dataProviderTestOutputPerMode 24 | */ 25 | public function testGetIntegrationScript(string $versionName, string $expected): void 26 | { 27 | $builder = new IntegrationScriptBuilder( 28 | new IntegrationVersionFactory() 29 | ); 30 | 31 | $result = $builder->getIntegrationScript($versionName, self::PARAMS); 32 | $this->assertHtmlEquals($expected, $result); 33 | } 34 | 35 | public static function dataProviderTestOutputPerMode(): array 36 | { 37 | return [ 38 | [ 39 | Pattern\CmpV2Tcf::VERSION_NAME, 40 | '' 45 | ], 46 | [ 47 | Pattern\CmpV2TcfLegacy::VERSION_NAME, 48 | '' 53 | ], 54 | [ 55 | Pattern\CmpV2Legacy::VERSION_NAME, 56 | '' 60 | ], 61 | [ 62 | Pattern\CmpV2::VERSION_NAME, 63 | '' 67 | ], 68 | [ 69 | Pattern\CmpV1::VERSION_NAME, 70 | '' 73 | ] 74 | ]; 75 | } 76 | 77 | public function testNoUsercentricsScriptInCustomMode(): void 78 | { 79 | $builder = new IntegrationScriptBuilder( 80 | new IntegrationVersionFactory() 81 | ); 82 | 83 | $versionName = Pattern\Custom::VERSION_NAME; 84 | $result = $builder->getIntegrationScript($versionName, self::PARAMS); 85 | 86 | $this->assertEmpty($result); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oxid-professional-services/usercentrics", 3 | "description": " OXID Cookie Management powered by usercentrics", 4 | "type": "oxideshop-module", 5 | "license": ["proprietary"], 6 | "minimum-stability": "dev", 7 | "prefer-stable": true, 8 | "require": { 9 | "php": "^8.2", 10 | "symfony/yaml" : "> 3", 11 | "ext-json": "*", 12 | "symfony/filesystem": "^6.0" 13 | }, 14 | "require-dev": { 15 | "ext-dom": "*", 16 | "ext-libxml": "*", 17 | "oxid-esales/oxideshop-ce": "dev-b-7.4.x", 18 | "oxid-esales/twig-component": "dev-b-7.4.x", 19 | "phpunit/phpunit": "^10.5", 20 | "mikey179/vfsstream": "~1.6.8", 21 | "phpstan/phpstan": "^1.10", 22 | "squizlabs/php_codesniffer": "^3.8", 23 | "phpmd/phpmd": "^2.15", 24 | "codeception/codeception": "^5.1", 25 | "codeception/module-asserts": "^3.0", 26 | "codeception/module-db": "^3.1", 27 | "codeception/module-filesystem": "^3.0", 28 | "codeception/module-webdriver": "^4.0", 29 | "oxid-esales/codeception-modules": "dev-b-7.4.x", 30 | "oxid-esales/codeception-page-objects": "dev-b-7.4.x", 31 | "oxid-esales/developer-tools": "dev-b-7.4.x" 32 | }, 33 | "conflict": { 34 | "oxid-esales/oxideshop-ce": "<7.4" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "OxidProfessionalServices\\Usercentrics\\": "src" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "OxidProfessionalServices\\Usercentrics\\Tests\\": "tests/", 44 | "OxidEsales\\EshopCommunity\\Tests\\": "./vendor/oxid-esales/oxideshop-ce/tests" 45 | } 46 | }, 47 | "scripts": { 48 | "phpcs": "phpcs --standard=tests/phpcs.xml --report=full", 49 | "phpcs-report": "phpcs --standard=tests/phpcs.xml --report=json --report-file=tests/Reports/phpcs.report.json", 50 | "phpcbf": "phpcbf --standard=tests/phpcs.xml", 51 | 52 | "phpstan": "phpstan -ctests/PhpStan/phpstan.neon analyse src/", 53 | "phpstan-report": "phpstan -ctests/PhpStan/phpstan.neon analyse src/ --error-format=json >tests/Reports/phpstan.report.json", 54 | 55 | "phpmd": "phpmd src ansi tests/PhpMd/standard.xml --ignore-errors-on-exit --ignore-violations-on-exit", 56 | "phpmd-report": "phpmd src json tests/PhpMd/standard.xml --ignore-errors-on-exit --ignore-violations-on-exit --reportfile tests/Reports/phpmd.report.json", 57 | 58 | "static": [ 59 | "@phpcs", 60 | "@phpstan", 61 | "@phpmd" 62 | ], 63 | 64 | "tests-unit": "XDEBUG_MODE=coverage vendor/bin/phpunit --config=tests/ --testsuite=Unit --coverage-clover=tests/Reports/coverage_unit_module-usercentrics.xml", 65 | "tests-integration": "XDEBUG_MODE=coverage vendor/bin/phpunit --bootstrap=/var/www/source/bootstrap.php --config=tests/ --testsuite=Integration --coverage-clover=tests/Reports/coverage_integration_module-usercentrics.xml", 66 | "tests-coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text --bootstrap=/var/www/source/bootstrap.php --config=tests/ --coverage-html=tests/result/Reports/CoverageHtml", 67 | 68 | "tests-codeception": [ 69 | "Composer\\Config::disableProcessTimeout", 70 | "THEME_ID=apex MODULE_IDS=oxps_usercentrics vendor/bin/codecept run Acceptance -c tests/codeception.yml --no-redirect" 71 | ], 72 | "tests-all": [ 73 | "@tests-unit", 74 | "@tests-integration", 75 | "@tests-codeception" 76 | ] 77 | }, 78 | "config": { 79 | "allow-plugins": { 80 | "oxid-esales/oxideshop-unified-namespace-generator": true, 81 | "oxid-esales/oxideshop-composer-plugin": true 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Service/ScriptServiceMapper.php: -------------------------------------------------------------------------------- 1 | 'SomeConfiguredServiceId'] 20 | * 21 | * @var array 22 | */ 23 | private array $scriptPathToService; 24 | 25 | /** 26 | * @var array 27 | */ 28 | private array $snippetToService; 29 | 30 | public function __construct(private readonly ConfigurationDaoInterface $configurationDao) 31 | { 32 | $this->scriptPathToService = $this->mapScriptPathsToServices(); 33 | $this->snippetToService = $this->mapScriptSnippetToServices(); 34 | } 35 | 36 | public function getServiceByScriptUrl(string $url): ?Service 37 | { 38 | foreach ($this->scriptPathToService as $path => $service) { 39 | if (preg_match($this->prepareScriptPathRegex($path), $url)) { 40 | return $service; 41 | } 42 | } 43 | 44 | return null; 45 | } 46 | 47 | /** 48 | * Build a regex that will match if the URL's path ends with this path 49 | */ 50 | private function prepareScriptPathRegex(string $path): string 51 | { 52 | return '/' . preg_quote($path, '/') . '(:?\?|#|$)/S'; 53 | } 54 | 55 | public function getServiceBySnippetId(string $snippetId): ?Service 56 | { 57 | return $this->snippetToService[$snippetId] ?? null; 58 | } 59 | 60 | 61 | protected function getServiceById(string $serviceId): ?Service 62 | { 63 | $config = $this->configurationDao->getConfiguration(); 64 | $services = $config->getServices(); 65 | 66 | return $services[$serviceId] ?? null; 67 | } 68 | 69 | /** 70 | * Gives an array like ["some/path/to/script.js" => 'someServiceId'] 71 | * 72 | * @return array 73 | */ 74 | private function mapScriptPathsToServices(): array 75 | { 76 | $config = $this->configurationDao->getConfiguration(); 77 | $scripts = $config->getScripts(); 78 | $result = []; 79 | 80 | foreach ($scripts as $oneScript) { 81 | $serviceId = $oneScript->getServiceId(); 82 | $path = $oneScript->getPath(); 83 | $result[$path] = $this->getServiceById($serviceId); 84 | } 85 | 86 | return $result; 87 | } 88 | 89 | /** 90 | * Gives an array like ['someSnippetId' => 'someServiceId'] 91 | * 92 | * @return array 93 | */ 94 | private function mapScriptSnippetToServices(): array 95 | { 96 | $config = $this->configurationDao->getConfiguration(); 97 | $scripts = $config->getScriptSnippets(); 98 | $result = []; 99 | 100 | foreach ($scripts as $oneScript) { 101 | $serviceId = $oneScript->getServiceId(); 102 | $result[$oneScript->getId()] = $this->getServiceById($serviceId); 103 | } 104 | 105 | return $result; 106 | } 107 | 108 | public function calculateSnippetId(string $snippetContents): string 109 | { 110 | return md5(trim($snippetContents)); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/Unit/Service/ModuleSettingsTest.php: -------------------------------------------------------------------------------- 1 | createPartialMock(ModuleSettingService::class, [$systemMethod]); 27 | $mssMock->expects($this->once())->method($systemMethod)->with( 28 | $key, 29 | Module::MODULE_ID 30 | )->willReturn($systemValue); 31 | 32 | $sut = new ModuleSettings($mssMock); 33 | $this->assertSame($expectedValue, $sut->$method()); 34 | } 35 | 36 | public static function gettersDataProvider(): array 37 | { 38 | return [ 39 | static::prepareStringTestItem('getUsercentricsId', ModuleSettings::USERCENTRICS_ID), 40 | static::prepareStringTestItem('getUsercentricsMode', ModuleSettings::USERCENTRICS_MODE), 41 | 42 | static::prepareArrayTestItem( 43 | 'getSmartProtectorBlockingDisabledServices', 44 | ModuleSettings::USERCENTRICS_SMART_DATA_PROTECTOR_BLOCKING_DISABLED 45 | ), 46 | 47 | ...static::prepareBoolTestItems( 48 | 'isSmartProtectorEnabled', 49 | ModuleSettings::USERCENTRICS_SMART_DATA_PROTECTOR_ENABLED, 50 | ), 51 | ...static::prepareBoolTestItems( 52 | 'isDevelopmentAutoConsentEnabled', 53 | ModuleSettings::USERCENTRICS_DEVELOPMENT_AUTO_CONSENT, 54 | ), 55 | ]; 56 | } 57 | 58 | private static function prepareBoolTestItems(string $method, string $key): array 59 | { 60 | return [ 61 | self::prepareBoolTestItem($method, $key, true), 62 | self::prepareBoolTestItem($method, $key, false), 63 | ]; 64 | } 65 | 66 | private static function prepareBoolTestItem(string $method, string $key, bool $value): array 67 | { 68 | return [ 69 | 'method' => $method, 70 | 'systemMethod' => 'getBoolean', 71 | 'key' => $key, 72 | 'systemValue' => $value, 73 | 'expectedValue' => $value 74 | ]; 75 | } 76 | 77 | private static function prepareStringTestItem(string $method, string $key): array 78 | { 79 | $exampleValue = 'exampleValue'; 80 | return [ 81 | 'method' => $method, 82 | 'systemMethod' => 'getString', 83 | 'key' => $key, 84 | 'systemValue' => new UnicodeString($exampleValue), 85 | 'expectedValue' => $exampleValue 86 | ]; 87 | } 88 | 89 | private static function prepareArrayTestItem(string $method, string $key): array 90 | { 91 | $exampleValue = 'exampleValue, nextExampleValue'; 92 | $expected = ['exampleValue', 'nextExampleValue']; 93 | return [ 94 | 'method' => $method, 95 | 'systemMethod' => 'getString', 96 | 'key' => $key, 97 | 'systemValue' => new UnicodeString($exampleValue), 98 | 'expectedValue' => $expected 99 | ]; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/Integration/Service/RendererTest.php: -------------------------------------------------------------------------------- 1 | createRenderer($file); 28 | $rendered = $sut->formFilesOutput([0 => ["http://shop.de/out/theme/js/test.js"]], ""); 29 | 30 | $this->assertStringContainsString( 31 | '', 32 | $rendered 33 | ); 34 | } 35 | 36 | public function testServiceNamedScript(): void 37 | { 38 | $file = 'Service1.yaml'; 39 | $sut = $this->createRenderer($file); 40 | $rendered = $sut->formFilesOutput([0 => ["https://shop.de/out/theme/js/test1.js"]], ""); 41 | 42 | $this->assertStringContainsString( 43 | '', 44 | $rendered 45 | ); 46 | } 47 | 48 | public function testNoScript(): void 49 | { 50 | $file = 'Service1.yaml'; 51 | $sut = $this->createRenderer($file); 52 | $rendered = $sut->formFilesOutput([], ""); 53 | 54 | $this->assertEmpty($rendered); 55 | } 56 | 57 | public function testServiceNamedSnippet(): void 58 | { 59 | $file = 'Snippets.yaml'; 60 | $sut = $this->createRenderer($file); 61 | $rendered = $sut->encloseScriptSnippet("alert('Service2')", "", false); 62 | 63 | $expectedResult = <<<'HTML' 64 | 67 | HTML; 68 | $expectedResult = str_replace("\n", '', $expectedResult); 69 | $this->assertStringContainsString($expectedResult, $rendered); 70 | } 71 | 72 | public function testNoSnippet(): void 73 | { 74 | $file = 'Snippets.yaml'; 75 | $sut = $this->createRenderer($file); 76 | $rendered = $sut->encloseScriptSnippet("", "", false); 77 | 78 | $this->assertEmpty($rendered); 79 | } 80 | 81 | public function testFormFilesOutputDoesNotSupportWidgets(): void 82 | { 83 | $this->expectException(WidgetsNotSupported::class); 84 | 85 | $file = 'Snippets.yaml'; 86 | $sut = $this->createRenderer($file); 87 | $sut->formFilesOutput([], "widgetName"); 88 | } 89 | 90 | public function testEncloseScriptSnippetDoesNotSupportWidgets(): void 91 | { 92 | $this->expectException(WidgetsNotSupported::class); 93 | 94 | $file = 'Snippets.yaml'; 95 | $sut = $this->createRenderer($file); 96 | $sut->encloseScriptSnippet("", "widgetName", false); 97 | } 98 | 99 | protected function createRenderer(string $file): Renderer 100 | { 101 | $config = new ConfigurationDao($this->getStorage($file, __DIR__ . '/ConfigTestData')); 102 | $scriptServiceMapper = new ScriptServiceMapper($config); 103 | return new Renderer($scriptServiceMapper); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /metadata.php: -------------------------------------------------------------------------------- 1 | 'oxps_usercentrics', 14 | 'title' => 'OXID Cookie Management powered by usercentrics', 15 | 'description' => [ 16 | 'de' => 'Die Usercentrics Consent Management Platform (CMP) ermöglicht Ihnen, Ihre Marketing- und Datenstrategie 17 | mit rechtlichen Anforderungen in Einklang zu bringen.

18 |

Registrieren Sie sich deshalb jetzt bei Usercentrics

19 |
20 | 21 | 22 |

23 | Sollte ein anderer Mitarbeiter in Ihrem Unternehmen die Registrierung durchführen, bitte dabei zwingend die OXID Partner-ID 16967 angeben, um die Integration vollständig nutzen zu können. Zu diesem Zweck können Sie diesen Link weitergeben: https://usercentrics.com/de/preise/?partnerid=16967#business-paket 24 |

25 |
26 | ', 27 | 'en' => 'The Usercentrics Consent Management Platform (CMP) enables you to harmonize your marketing and data 28 | strategy with legal requirements.

29 |

Register now for Usercentrics

30 |
31 | 32 | 33 |

34 | If another employee in your company registers, please make sure to enter the OXID partner ID 16967 in order to be able to fully use the integration. For that reason you can forward this link to them: https://usercentrics.com/pricing/?partnerid=16967#business-package 35 |

36 |
37 | ' 38 | ], 39 | 'thumbnail' => 'logo.png', 40 | 'version' => '3.2.1', 41 | 'author' => 'OXID Professional Services', 42 | 'events' => [], 43 | 44 | 'templates' => [], 45 | 46 | 'settings' => [ 47 | [ 48 | 'group' => 'usercentrics_main', 49 | 'name' => 'usercentricsId', 50 | 'type' => 'str', 51 | 'value' => '' 52 | ], 53 | [ 54 | 'group' => 'usercentrics_advanced', 55 | 'name' => 'smartDataProtectorActive', 56 | 'type' => 'bool', 57 | 'value' => true 58 | ], 59 | [ 60 | 'group' => 'usercentrics_advanced', 61 | 'name' => 'smartDataProtectorDeactivateBlocking', 62 | 'type' => 'str', 63 | 'value' => '' 64 | ], 65 | [ 66 | 'group' => 'usercentrics_advanced', 67 | 'name' => 'usercentricsMode', 68 | 'type' => 'select', 69 | 'value' => Pattern\CmpV2::VERSION_NAME, 70 | 'constraints' => 71 | Pattern\CmpV1::VERSION_NAME . '|' . 72 | Pattern\CmpV2::VERSION_NAME . '|' . 73 | Pattern\CmpV2Legacy::VERSION_NAME . '|' . 74 | Pattern\CmpV2Tcf::VERSION_NAME . '|' . 75 | Pattern\CmpV2TcfLegacy::VERSION_NAME . '|' . 76 | Pattern\Custom::VERSION_NAME 77 | ], 78 | [ 79 | 'group' => '', 80 | 'name' => 'developmentAutomaticConsent', 81 | 'type' => 'bool', 82 | 'value' => false 83 | ], 84 | ], 85 | 86 | 'controllers' => [], 87 | 88 | 'extend' => [ 89 | JavaScriptRenderer::class => ScriptRenderer::class, 90 | ViewConfig::class => UsercentricsViewConfig::class 91 | ] 92 | ]; 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OXID Cookie Management powered by usercentrics 2 | 3 | [![Packagist](https://img.shields.io/packagist/v/oxid-professional-services/usercentrics.svg)](https://packagist.org/packages/oxid-professional-services/usercentrics) 4 | 5 | This module provides the [Usercentrics](https://usercentrics.com/de/preise/?partnerid=16967#business-paket) functionality for the [OXID eShop](https://www.oxid-esales.com/) allowing you to use their Consent Management Platform. 6 | 7 | ## Usage 8 | 9 | This assumes you have OXID eShop (at least the `v6.2.0` compilation) up and running. 10 | 11 | ### Install 12 | 13 | The Usercentrics module is already included in the OXID eShop compilation. 14 | 15 | Module can be installed manually, by using composer: 16 | ```bash 17 | $ composer require oxid-professional-services/usercentrics 18 | ``` 19 | 20 | After requiring the module, you need to activate it, either via OXID eShop admin or CLI. 21 | 22 | Navigate to oxideshop folder and execute the following: 23 | ```bash 24 | $ vendor/bin/oe-console oe:module:activate oxps_usercentrics 25 | ``` 26 | 27 | ### How to use 28 | 29 | Activate the module and enter your usercentrics ID in the module settings. 30 | 31 | User documentation: [DE](https://docs.oxid-esales.com/modules/usercentrics/de/latest/) 32 | 33 | ## Branch Compatibility 34 | 35 | * b-7.4.x branch and v3.2.1 for b-7.4.x shop compilation branches 36 | * b-7.3.x branch and v3.1.0 for b-7.3.x shop compilation branches 37 | * b-7.2.x branch and v3.0.0 for b-7.2.x shop compilation branches 38 | * b-7.1.x branch and v3.0.0 for b-7.1.x shop compilation branches 39 | * b-7.0.x branch and v3.0.0 for b-7.0.x shop compilation branches 40 | * b-6.5.x branch for b-6.5.x shop compilation branches 41 | * b-6.3.x branch for b-6.3.x and b-6.4.x shop compilation branches 42 | * b-6.2.x branch for b-6.2.x shop compilation branches 43 | 44 | ### Development installation 45 | 46 | We recommend developing the module as independent as possible. This means that the module for development should 47 | be installed as a [root package](https://getcomposer.org/doc/04-schema.md#root-package), with its own strict dependencies if such are needed. 48 | 49 | The next section shows how to install the module as a root package by using the OXID eShop SDK. 50 | 51 | In case of different environment usage, please adjust by your own needs. 52 | 53 | ### Development installation on OXID eShop SDK 54 | 55 | The installation instructions below are shown for the current [SDK](https://github.com/OXID-eSales/docker-eshop-sdk) 56 | for shop 7.4. Make sure your system meets the requirements of the SDK. 57 | 58 | 0. Ensure all docker containers are down to avoid port conflicts 59 | 60 | 1. Clone the SDK for the new project 61 | ```shell 62 | echo MyProject && git clone https://github.com/OXID-eSales/docker-eshop-sdk.git $_ && cd $_ 63 | ``` 64 | 65 | 2. Clone the repository to the source directory 66 | ```shell 67 | git clone --recurse-submodules https://github.com/OXID-eSales/usercentrics.git --branch=b-7.4.x ./source 68 | ``` 69 | 70 | 3. Run the recipe to setup the development environment, you can decide which shop edition to install. Omitting the flag installs EE. 71 | ```shell 72 | ./source/recipes/setup-development.sh 73 | ``` 74 | 75 | You should be able to access the shop with http://localhost.local and the admin panel with http://localhost.local/admin 76 | (credentials: noreply@oxid-esales.com / admin) 77 | 78 | ## Testing 79 | ### Linting, syntax check, static analysis 80 | 81 | ```bash 82 | $ composer update 83 | $ composer static 84 | ``` 85 | 86 | ### Unit/Integration/Acceptance tests 87 | 88 | - Install this module in a running OXID eShop 89 | - Reset the shop's database 90 | 91 | ```bash 92 | $ bin/oe-console oe:database:reset --db-host=db-host --db-port=db-port --db-name=db-name --db-user=db-user --db-password=db-password --force 93 | ``` 94 | 95 | - Run all the tests 96 | 97 | ```bash 98 | $ composer tests-all 99 | ``` 100 | 101 | - Or the desired suite 102 | 103 | ```bash 104 | $ composer tests-unit 105 | $ composer tests-integration 106 | $ composer tests-codeception 107 | ``` 108 | ## Contributing 109 | 110 | You like to contribute? 🙌 AWESOME 🙌\ 111 | Go and check the [contribution guidelines](CONTRIBUTING.md) 112 | 113 | ## Issues 114 | 115 | To report issues with the module, please use the [OXID eShop bugtracking system](https://bugs.oxid-esales.com/) - module Usercentrics project. 116 | 117 | ## License 118 | 119 | OXID Module and Component License, see [LICENSE file](LICENSE). 120 | -------------------------------------------------------------------------------- /tests/Integration/Service/ScriptServiceMapperTest.php: -------------------------------------------------------------------------------- 1 | createScriptMapper('Service1.yaml'); 29 | 30 | $service = $scriptServiceMapper->getServiceByScriptUrl($scriptUrl); 31 | $this->assertNull( 32 | $service, 33 | "test.js should not return a service name as its not configured" 34 | ); 35 | } 36 | 37 | /** 38 | * @dataProvider matchingScriptUrls 39 | */ 40 | public function testScriptNameConfigured(string $scriptUrl): void 41 | { 42 | $scriptServiceMapper = $this->createScriptMapper('Service1.yaml'); 43 | 44 | /** @var Service $service */ 45 | $service = $scriptServiceMapper->getServiceByScriptUrl($scriptUrl); 46 | 47 | $this->assertNotNull($service); 48 | $this->assertEquals("name1", $service->getName()); 49 | } 50 | 51 | public static function matchingScriptUrls(): array 52 | { 53 | return [ 54 | ["http://someurl/path/test1.js"], 55 | ["http://someurl/path/test1.js?123456"], 56 | ["http://someurl/path/test1.js?123456#abc"], 57 | ["http://someurl/path/test1.js#abc"], 58 | ["https://someurl/path/test2.js#abc"], 59 | ["https://someurl/1/test/js/path/test2.js?123456"], 60 | ]; 61 | } 62 | 63 | public static function notMatchingScriptUrls(): array 64 | { 65 | return [ 66 | ["http://someurl/path/test.js"], 67 | ["http://someurl/path/test.js?123456"], 68 | ["http://someurl/path/test.js?123456#abc"], 69 | ["http://someurl/path/test.js#abc"], 70 | ["https://someurl/test2.js#abc"], 71 | ["https://someurl/js/test2.js"], 72 | ["https://someurl/path/js/test2.js?123456"], 73 | ]; 74 | } 75 | 76 | 77 | public function testCalculateSnippetIdIsNotEmpty(): void 78 | { 79 | $scriptServiceMapper = $this->createScriptMapper('Snippets.yaml'); 80 | $snippet = "alert('Service1')"; 81 | $id = $scriptServiceMapper->calculateSnippetId($snippet); 82 | $this->assertNotEmpty($id); 83 | } 84 | 85 | public function testCalculateSnippetIdIsUnique(): void 86 | { 87 | $scriptServiceMapper = $this->createScriptMapper('Snippets.yaml'); 88 | $snippet = "alert('Service1')"; 89 | $id = $scriptServiceMapper->calculateSnippetId($snippet); 90 | 91 | $snippet2 = "alert('Service2')"; 92 | $id2 = $scriptServiceMapper->calculateSnippetId($snippet2); 93 | $this->assertNotEquals($id, $id2); 94 | } 95 | 96 | public function testCalculateSnippetIdIsStable(): void 97 | { 98 | $scriptServiceMapper = $this->createScriptMapper('Snippets.yaml'); 99 | $snippet = "alert('Service1')"; 100 | $id = $scriptServiceMapper->calculateSnippetId($snippet); 101 | $id3 = $scriptServiceMapper->calculateSnippetId($snippet); 102 | $this->assertEquals($id, $id3); 103 | } 104 | 105 | public function testGetServiceBySnippetId(): void 106 | { 107 | $scriptServiceMapper = $this->createScriptMapper('Snippets.yaml'); 108 | $id = $scriptServiceMapper->calculateSnippetId("alert('Service2')"); 109 | $service = $scriptServiceMapper->getServiceBySnippetId($id); 110 | $this->assertNotNull($service); 111 | /** @psalm-suppress PossiblyNullReference */ 112 | $this->assertEquals("name1", $service->getName()); 113 | } 114 | 115 | public function testGetServiceByNotExistingSnippetId(): void 116 | { 117 | $scriptServiceMapper = $this->createScriptMapper('Snippets.yaml'); 118 | $id = $scriptServiceMapper->calculateSnippetId("alert('NoService')"); 119 | $service = $scriptServiceMapper->getServiceBySnippetId($id); 120 | $this->assertNull($service); 121 | } 122 | 123 | private function createScriptMapper(string $file): ScriptServiceMapper 124 | { 125 | $config = new ConfigurationDao($this->getStorage($file, __DIR__ . '/ConfigTestData')); 126 | return new ScriptServiceMapper($config); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [3.2.1] - 2025-10-14 8 | 9 | ### Changed 10 | - Updated composer.json with stable release versions 11 | 12 | ## [3.2.0] - 2025-10-13 13 | 14 | ### Changed 15 | - Updated to work with OXID eShop 7.4.x. 16 | 17 | ## [3.1.0] - 2025-04-11 18 | 19 | ### Added 20 | - Support for PHP 8.4. 21 | 22 | ### Changed 23 | - Prevent page blocking when Usercentrics ID is missing 24 | - Updated to work with OXID eShop 7.3.x. 25 | 26 | ### Removed 27 | - Dropped support for PHP 8.1. 28 | 29 | ## [3.0.0] - 2024-03-15 30 | 31 | ### Added 32 | - ``Core\ViewConfig::getUsercentricsModuleSettings`` 33 | - ``Service\Integration\Pattern\*->scriptSource`` type definition 34 | - ``Service/ModuleSettingsInterface::getSmartProtectorBlockingDisabledServices`` 35 | - Code style tools 36 | 37 | ### Removed 38 | - Smarty support 39 | - ``Core\ViewConfig`` methods: 40 | - ``getUsercentricsID`` 41 | - ``isSmartDataProtectorActive`` 42 | - ``isDevelopmentAutomaticConsentActive`` 43 | - ``getSmartDataProtectorDeactivateBlockingServices`` 44 | 45 | ### Changed 46 | - Rename ``getSmartProtectorBlockingDisabledList`` to ``getSmartProtectorBlockingDisabledServices`` and change return 47 | type to ``string[]`` for: 48 | - ``Service/ModuleSettingsInterface`` 49 | - ``Service/ModuleSettings`` 50 | - Change ``Service/ModuleSettings::getStringSettingValue`` from protected to private 51 | 52 | ## [2.0.2] - 2023-05-09 53 | 54 | ### Fixed 55 | - Fix version in metadata 56 | - Fix version links in the changelog 57 | 58 | ## [2.0.1] - 2023-05-05 59 | 60 | ### Changed 61 | - License updated - OXID Module and Component License instead of GPL 62 | - Readme updated to clarify shop version compatibility 63 | 64 | ## [2.0.0] - 2023-05-04 65 | 66 | ### Added 67 | - Supports Twig shop configuration 68 | - Github workflow with tests runs added 69 | - PHP 8.0 and 8.1 support 70 | 71 | ### Changed 72 | - Not autowired container access goes through the `OxidProfessionalServices\Usercentrics\Traits\ServiceContainer` trait 73 | - `OxidProfessionalServices\Usercentrics\Service\ModuleSettings` improved: 74 | - Every module setting have its own getter 75 | - New module settings service used to access module configurations 76 | - Tests are not based on testing library anymore 77 | 78 | ### Fixed 79 | - Templates extracted from `src` directory and moved one level up 80 | 81 | ### Removed 82 | - Php 7.x are not supported anymore 83 | 84 | ## [1.2.2] - unreleased 85 | 86 | ### Changed 87 | - License updated - now using OXID ESS Module License 88 | 89 | ## [1.2.1] - 2022-06-07 90 | 91 | ### Fixed 92 | - `OxidProfessionalServices\Usercentrics\Service\Integration\IntegrationScriptBuilder` constructor's type-hint 93 | 94 | ## [1.2.0] - 2021-11-03 95 | 96 | ### Added 97 | - Add [deactivateBlocking configuration](https://docs.usercentrics.com/#/smart-data-protector?id=deactivate-smart-data-protector-for-specific-services) feature 98 | - Development related hidden parameter developmentAutomaticConsent 99 | 100 | ### Fixed 101 | - Rework tests to work with UserCentrics CMPv2 102 | - Possibility to run tests with new chrome browser 103 | - Fix possible test runner environment constants names to fit testing library documentation 104 | 105 | ## [1.1.3] - 2021-04-12 106 | 107 | ### Fixed 108 | - Fixed tests for never phpunit versions 109 | 110 | ## [1.1.2] - 2021-03-10 111 | 112 | ### Changed 113 | - Admin area: the link with the partnerid that is showed points directly to price and order form from usercentrics 114 | 115 | ## [1.1.1] - 2021-03-03 116 | 117 | ### Changed 118 | - Admin area: open API documentation in new browser tab 119 | - Admin area: added registration link 120 | 121 | ### Fixed 122 | - Fix default value for usercentricsMode 123 | - Improved the tests to be more stable on different shop modules configurations 124 | 125 | ## [1.1.0] - 2021-01-19 126 | 127 | ### Added 128 | - Support for Usercentrics CmpV2 including legacy browser mode. 129 | - Support for Usercentrics CmpV2 TFC (experimental). 130 | - ``Service\ModuleSettings`` class for accessing this module settings. 131 | 132 | ### Deprecated 133 | - ``Core\ViewConfig::getUsercentricsID`` 134 | 135 | ### Changed 136 | - ``ModuleSettingsInterface`` is used to access module settings in the shop. 137 | 138 | ### Fixed 139 | - Tests improved and cleaned up. 140 | - Added tests for several edge cases. 141 | 142 | ## [1.0.0] - 2020-12-09 143 | 144 | ### Added 145 | - Module provides a possibility to turn on "Smart data protection" function provided by UserCentrics. 146 | - Possibility to configure any javascript included with oxscript tag to usercentrics service, and allow client to manipulate (turn it on/off) by Usercentrics data protection panel. 147 | 148 | [3.2.1]: https://github.com/OXID-eSales/usercentrics/compare/v3.2.0...v3.2.1 149 | [3.2.0]: https://github.com/OXID-eSales/usercentrics/compare/v3.1.0...v3.2.0 150 | [3.1.0]: https://github.com/OXID-eSales/usercentrics/compare/v3.0.0...v3.1.0 151 | [3.0.0]: https://github.com/OXID-eSales/usercentrics/compare/v2.0.2...v3.0.0 152 | [2.0.2]: https://github.com/OXID-eSales/usercentrics/compare/v2.0.1...v2.0.2 153 | [2.0.1]: https://github.com/OXID-eSales/usercentrics/compare/v2.0.0...v2.0.1 154 | [2.0.0]: https://github.com/OXID-eSales/usercentrics/compare/v1.2.1...v2.0.0 155 | [1.2.2]: https://github.com/OXID-eSales/usercentrics/compare/v1.2.1...b-6.5.x 156 | [1.2.1]: https://github.com/OXID-eSales/usercentrics/compare/v1.2.0...v1.2.1 157 | [1.2.0]: https://github.com/OXID-eSales/usercentrics/compare/v1.1.3...v1.2.0 158 | [1.1.3]: https://github.com/OXID-eSales/usercentrics/compare/v1.1.2...v1.1.3 159 | [1.1.2]: https://github.com/OXID-eSales/usercentrics/compare/v1.1.1...v1.1.2 160 | [1.1.1]: https://github.com/OXID-eSales/usercentrics/compare/v1.1.0...v1.1.1 161 | [1.1.0]: https://github.com/OXID-eSales/usercentrics/compare/v1.0.0...v1.1.0 162 | [1.0.0]: https://github.com/OXID-eSales/usercentrics/commits/v1.0.0 163 | -------------------------------------------------------------------------------- /src/Service/Configuration/ConfigurationDao.php: -------------------------------------------------------------------------------- 1 | getScriptsConfiguration(); 26 | $scriptsSnippets = $this->getScriptSnippetsConfiguration(); 27 | $services = $this->getServicesConfiguration(); 28 | 29 | return new Configuration($services, $scripts, $scriptsSnippets); 30 | } 31 | 32 | /** 33 | * @return Script[] 34 | */ 35 | private function getScriptsConfiguration(): array 36 | { 37 | $plainConfig = $this->storage->getData(); 38 | $plainScripts = $this->getConfigTypeFromPlainData('scripts', $plainConfig); 39 | 40 | $scripts = []; 41 | /** @var string[] $scriptDataArray */ 42 | foreach ($plainScripts as $scriptDataArray) { 43 | $scripts[] = $this->scriptFromArray($scriptDataArray); 44 | } 45 | 46 | return $scripts; 47 | } 48 | 49 | /** 50 | * @return ScriptSnippet[] 51 | */ 52 | private function getScriptSnippetsConfiguration(): array 53 | { 54 | $plainConfig = $this->storage->getData(); 55 | $plainScripts = $this->getConfigTypeFromPlainData('scriptSnippets', $plainConfig); 56 | 57 | $scriptSnippets = []; 58 | /** @var string[] $scriptDataArray */ 59 | foreach ($plainScripts as $scriptDataArray) { 60 | $scriptSnippets[] = $this->scriptSnippetFromArray($scriptDataArray); 61 | } 62 | 63 | return $scriptSnippets; 64 | } 65 | 66 | /** 67 | * @return Service[] 68 | */ 69 | private function getServicesConfiguration(): array 70 | { 71 | $plainConfig = $this->storage->getData(); 72 | $plainServices = $this->getConfigTypeFromPlainData('services', $plainConfig); 73 | 74 | $services = []; 75 | /** @var string[] $serviceDataArray */ 76 | foreach ($plainServices as $serviceDataArray) { 77 | $service = $this->serviceFromArray($serviceDataArray); 78 | $services[$service->getId()] = $service; 79 | } 80 | 81 | return $services; 82 | } 83 | 84 | /** 85 | * @param string $typeOfList its a key - scripts|services 86 | * @param mixed[] $plainConfig 87 | * 88 | * @return mixed[] 89 | */ 90 | private function getConfigTypeFromPlainData(string $typeOfList, array $plainConfig): array 91 | { 92 | /** @var mixed $typeConfig */ 93 | $typeConfig = $plainConfig[$typeOfList] ?? []; 94 | 95 | if (!is_array($typeConfig)) { 96 | $typeConfig = []; 97 | } 98 | 99 | return $typeConfig; 100 | } 101 | 102 | /** 103 | * @param mixed[] $data 104 | */ 105 | private function scriptFromArray(array $data): Script 106 | { 107 | $path = (string)($data['path'] ?? ''); 108 | $service = (string)($data['service'] ?? ''); 109 | 110 | return new Script($path, $service); 111 | } 112 | 113 | /** 114 | * @SuppressWarnings(PHPMD.ShortVariable) 115 | * @param mixed[] $data 116 | */ 117 | private function scriptSnippetFromArray(array $data): ScriptSnippet 118 | { 119 | $id = (string)($data['id'] ?? ''); 120 | $service = (string)($data['service'] ?? ''); 121 | 122 | return new ScriptSnippet($id, $service); 123 | } 124 | 125 | /** 126 | * @param mixed[] $data 127 | */ 128 | private function serviceFromArray(array $data): Service 129 | { 130 | $serviceName = (string)($data['name'] ?? ''); 131 | $serviceId = (string)($data['id'] ?? ''); 132 | 133 | return new Service($serviceName, $serviceId); 134 | } 135 | 136 | public function putConfiguration(Configuration $configuration): void 137 | { 138 | $plainConfig = [ 139 | 'scripts' => $this->preparePlainScriptsArray($configuration->getScripts()), 140 | 'services' => $this->preparePlainServicesArray($configuration->getServices()), 141 | 'scriptSnippets' => $this->preparePlainSnippetsArray($configuration->getScriptSnippets()) 142 | ]; 143 | 144 | $this->storage->putData($plainConfig); 145 | } 146 | 147 | /** 148 | * Converts array of Services to plain array for further saving 149 | * 150 | * @param Script[] $scripts 151 | * 152 | * @return mixed[] 153 | */ 154 | private function preparePlainScriptsArray(array $scripts): array 155 | { 156 | $plainScripts = []; 157 | 158 | foreach ($scripts as $script) { 159 | $plainScripts[] = [ 160 | 'service' => $script->getServiceId(), 161 | 'path' => $script->getPath() 162 | ]; 163 | } 164 | 165 | return $plainScripts; 166 | } 167 | 168 | /** 169 | * Converts array of Services to plain array for further saving 170 | * 171 | * @param Service[] $services 172 | * 173 | * @return mixed[] 174 | */ 175 | private function preparePlainServicesArray(array $services): array 176 | { 177 | $plainServices = []; 178 | 179 | foreach ($services as $service) { 180 | $plainServices[] = [ 181 | 'name' => $service->getName(), 182 | 'id' => $service->getId() 183 | ]; 184 | } 185 | 186 | return $plainServices; 187 | } 188 | 189 | /** 190 | * Converts array of Services to plain array for further saving 191 | * 192 | * @param ScriptSnippet[] $snippets 193 | * 194 | * @return mixed[] 195 | */ 196 | private function preparePlainSnippetsArray(array $snippets): array 197 | { 198 | $plainSnippets = []; 199 | 200 | foreach ($snippets as $snippet) { 201 | $plainSnippets[] = [ 202 | 'service' => $snippet->getServiceId(), 203 | 'id' => $snippet->getId() 204 | ]; 205 | } 206 | 207 | return $plainSnippets; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | OXID ESS-Modul-Lizenz 2023 2 | 3 | 1. Präambel 4 | Diese Software ist ein ESS-Modul (E-Commerce Standard Service) und ausschließlich für den Einsatz mit den für kommerzielle Nutzung freigegebenen Editionen des OXID eShop bestimmt. Dies sind die Professional Edition und die Enterprise Edition, sowie die OXID eShop CE-Software, vorausgesetzt, für diese wird eine monatliche Nutzungsgebühr bezahlt. 5 | 6 | 2. Urheberrecht 7 | Die Software ist urheberrechtlich geschützt und geschütztes Geschäftsgeheimnis von OXID eSales. 8 | OXID eSales behält sich alle Rechte vor, sofern Ihnen in dieser Lizenzvereinbarung keine ausdrücklichen Rechte an der Software eingeräumt werden. 9 | 10 | 3. Rechteeinräumung 11 | OXID eSales räumt Ihnen an der Software das nicht ausschließliche und nicht übertragbare Recht zur Nutzung der Software ein. 12 | 13 | 4. Nutzungsbedingungen 14 | Sie erhalten das Recht zur Nutzung unter der Bedingung, dass Sie diese Lizenzvereinbarung und die darin enthaltenen Nutzungsbedingungen akzeptieren. 15 | 16 | 5. Einschränkungen 17 | a. Die Nutzung von unter dieser Lizenz veröffentlichten Softwareprodukten oder -modulen in Verbindung mit früheren Versionen der OXID eShop CE Software (vor Version 6.4.2) bzw. von dieser abgeleiteten Softwareprodukten (Forks) ist nicht zulässig. 18 | b. Die Nutzung dieser Software oder Teilen davon durch Module oder Software Dritter unterliegt diesen Nutzungsbedingungen. 19 | c. Die Software kann Softwareprodukte von Fremdherstellern enthalten. Die Fremdprodukte können Sie nach der Installation der composer.lock-Datei entnehmen. Für diese Fremdprodukte gelten zusätzliche Lizenzbestimmungen, die sie der entsprechenden Lizenzdatei des Fremdprodukts entnehmen können. 20 | d. Sie dürfen die Software weder umarbeiten, anpassen noch in eine andere Programmiersprache übersetzen, ebenso sind Reverse Engineering und Nachbau der Software nicht erlaubt. 21 | e. Jede gemäß dieser Lizenz zulässige Kopie der Software muss die Urheberrechts- und Schutzrechtsvermerke von OXID eSales tragen, die auf oder in der lizensierten Software vorhanden sind. 22 | f. Sie dürfen die Software Dritten weder vermieten, verleihen, unterlizensieren oder übertragen. 23 | g. Sie dürfen den Quellcode der Software Dritten nicht zugänglich machen, es sei denn, 24 | - die entsprechenden Dateien sind von OXID eSales explizit als öffentlich (open source) gekennzeichnet oder 25 | - der Dritte benötigt den Quellcode zur Durchführung seiner Tätigkeit für Sie und hat mit OXID eSales eine schriftliche Geheimhaltungsvereinbarung abgeschlossen. 26 | 27 | 6. Nutzungsuntersagung und Nutzungsentschädigung 28 | Wenn Sie die Bestimmungen von Ziff. 4 und 5 nicht oder nicht vollständig erfüllen, ist OXID eSales ohne weiteres berechtigt, Ihnen die Nutzung der Software zu untersagen. Schadensersatzansprüche von OXID eSales bleiben vorbehalten. 29 | Werden die Nutzungsbedingungen nicht eingehalten und gibt es keine andere Vereinbarung, stimmt der Lizenznehmer der jeweils gültigen Nutzungsentschädigung (Stand 31.12.2022: 111€) zuzüglich des Inflationsausgleichs gemäß der vom statistischen Bundesamt festgestellten jährlichen Inflationsrate für jeden Monat unrechtmäßiger Nutzung der Software mit anderen Softwareprodukten zu. Diese Entschädigung entbindet nicht vom Abschluss eines der Nutzung entsprechenden Lizenzvertrags. 30 | Für spätere Versionen behält sich OXID eine Anpassung der Nutzungsgebühr vor. 31 | Werden mehrere Shops oder Teile der OXID Professional Edition oder OXID Enterprise Edition oder vergleichbare Funktionalität, die durch Dritte auf Basis von OXID Software bereitgestellt wurde, mit dieser Software genutzt, wird eine Lizenzmiete für die Professional Edition oder Enterprise Edition gemäß der OXID Preisliste fällig. Es gilt die Preisliste des Monats, in dem die erste Lizenzmiete bezahlt wird. 32 | 33 | 7. Support und Wartung 34 | Support- und Wartungsleistungen sind nicht Bestandteil dieser Lizenzvereinbarung. Wenn Sie solche Leistungen in Anspruch nehmen wollen, wenden Sie sich bitte an sales@oxid-esales.com. 35 | 36 | 8. Rechte bei Mängeln 37 | a. Die vertragsgemäße Beschaffenheit der Software bestimmt sich ausschließlich nach den Spezifikationen der Dokumentation in der bei Abschluss dieses Lizenzvertrages gültigen Fassung. 38 | b. Die Verjährungsfrist beträgt 12 Monate nach Lieferung der Software (Bereitstellung zum Download). 39 | c. Die Software wird Ihnen in der Beschaffenheit überlassen, wie sie sich zum Zeitpunkt des Abschlusses dieses Lizenzvertrages befindet. OXID eSales übernimmt keine Haftung für Sach- und Rechtsmängel, insbesondere übernehmen wir keine Haftung für die vertragsgemäße Beschaffenheit oder die Eignung für den vertraglich vorausgesetzten Verwendungszweck. Die in der Dokumentation gegebene Beschreibung der Software darf in keinem Fall als Zusicherung oder Garantie verstanden werden. Unsere Haftung für vorsätzliches und/oder arglistiges Handeln bleibt davon jedoch unberührt. 40 | 41 | 9. Haftung 42 | a. OXID eSales haftet nur bei Vorsatz und grober Fahrlässigkeit, Ansprüchen nach dem deutschen Produkthaftungsgesetz sowie bei einer Verletzung des Lebens, des Körpers oder der Gesundheit. Im Übrigen haften die Parteien unbeschränkt nach den gesetzlichen Vorschriften 43 | b. Alle anderen Ansprüche sind ausgeschlossen. 44 | 45 | 10. Sonstiges 46 | a. Dieser Lizenzvertrag unterliegt deutschem Recht. 47 | b. Sollte eine Bestimmung dieses Lizenzvertrages unwirksam oder nichtig sein oder werden, so bleibt seine Wirksamkeit im Übrigen unberührt. 48 | c. Für die Auslegung dieses Lizenzvertrages ist die deutsche Sprachfassung verbindlich. 49 | d. Für alle Streitigkeiten, die aus dieser Lizenzvereinbarung entstehen, wird die Zuständigkeit des Landgerichts Freiburg vereinbart. 50 | 51 | 52 | 53 | ENGLISH VERSION 54 | 55 | This is a rough translation of the German language version for informational purposes. The German language version of this license agreement is legally binding. 56 | 57 | 1. Preamble 58 | This software is an ESS module (E-Commerce Standard Service) and is intended exclusively for use with the editions of OXID eShop released for commercial use. These are the Professional Edition and the Enterprise Edition, as well as the OXID eShop CE software, provided that a monthly fee is paid for these. 59 | 60 | 2. Copyright 61 | The software is copyrighted and protected trade secret of OXID eSales. 62 | OXID eSales reserves all rights unless you are granted explicit rights to the software in this license agreement. 63 | 64 | 3. Granting of rights 65 | OXID eSales grants you the non-exclusive and non-transferable right to use the software. 66 | 67 | 4. Terms of use 68 | You are granted the right to use the software under the condition that you accept this license agreement and the included terms of use. 69 | 70 | 5. Restrictions 71 | a. The use of software products or modules published under this licence in connection with earlier versions of the OXID eShop CE software (prior to version 6.4.2) or software products derived from it (forks) is not permitted. 72 | b. The use of this software or parts thereof by third party modules or software is subject to these Terms of Use. 73 | c. The software contains software products from third-party manufacturers. You can find the third-party products in the composer.lock file after installation. Additional license conditions apply to these third-party products, which you can find in the corresponding license file of the third-party product. 74 | d. You may not modify, adapt or translate this software, nor may you reverse engineer or decompile the software. 75 | e. Any copy of the Software permitted under this license must bear the copyright and proprietary notices of OXID eSales that are present on or in the Licensed Software. 76 | f. You may not rent, loan, sublicense or transfer the Software to any third party. 77 | g. You may not make the source code of the software available to third parties unless, 78 | - the corresponding files are explicitly marked as public (open source) by OXID eSales or 79 | - the third party requires the source code to carry out its activities for you and has concluded a written non-disclosure agreement with OXID eSales. 80 | 81 | 6. Prohibition of Use and Compensation for Use 82 | If you do not or not completely fulfill the provisions of clauses 4 and 5, OXID eSales is entitled without further ado to prohibit you from using the software. OXID eSales reserves the right to claim damages. 83 | If the terms of use are not complied with and there is no other agreement, the licensee agrees to compensation for use of € 111 plus inflation compensation in accordance with the annual inflation rate determined by the Federal Statistical Office of Germany for each month of unlawful use. This compensation does not release from the conclusion of a license agreement corresponding to the use. 84 | For later versions OXID reserves the right to adjust the usage fee. 85 | If several shops or parts of the OXID Professional Edition or OXID Enterprise Edition or comparable functionality provided by third parties based on OXID software are used, a license fee according to the OXID price list is due. The price list of the month in which the first license fee is paid applies. 86 | 87 | 7. Support and Maintenance 88 | Support and maintenance services are not part of this license agreement. If you wish to make use of such services, please contact sales@oxid-esales.com. 89 | 90 | 8. Rights in the Event of Defects 91 | a. The contractual quality of the software is determined exclusively by the specifications of the documentation in the version valid at the time of conclusion of this license agreement. 92 | b. The limitation period is 12 months after delivery of the software (provision for download). 93 | c. The software is provided to you in the condition as it is at the time of the conclusion of this license agreement. OXID eSales accepts no liability for material defects or defects of title, in particular we accept no liability for the contractual condition or suitability for the contractually assumed purpose. The description of the software given in the documentation may in no case be understood as an assurance or guarantee. However, this shall not affect our liability for intentional and/or fraudulent acts. 94 | 95 | 9. Liability 96 | a. OXID eSales shall only be liable in the event of intent and gross negligence, claims under the German Product Liability Act and in the event of injury to life or health. In all other respects the parties shall be liable without limitation in accordance with the statutory provisions. 97 | b. All other claims are excluded. 98 | 99 | 10. Miscellaneous 100 | a. This license agreement is subject to German law. 101 | b. Should any provision of this license agreement be or become invalid or void, the validity of the remaining provisions shall remain unaffected. 102 | c. The German language version shall be binding for the interpretation of this license agreement. 103 | d. It is agreed that the Regional Court of Freiburg shall have jurisdiction over all disputes arising from this licence agreement. --------------------------------------------------------------------------------