first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
<?php
$txt_str['restrictions'] = [
'toolbar_list' => 'Seznam omezení',
'toolbar_add' => 'Vytvořit omezení',
'name' => 'Popis omezení',
'producers' => 'Značky',
'categories' => 'Sekce',
'campaigns' => 'Kampaně',
'products' => 'Produkty',
'parameters_list' => 'Parametry',
'countries' => 'Země',
'users' => 'Uživatele',
'regions' => 'Regiony',
'login' => 'Přihlášení',
'price_levels' => 'Cenové hladiny',
'users_groups' => 'Skupiny uživatelů',
'login.logIn' => 'Přihlášený uživatel',
'login.logOut' => 'Nepřihlášený uživatel',
'domain' => 'Omezíme uživatele',
'products_hide' => 'Skryjeme produkty',
'products_show' => 'Odkryjeme pouze produkty',
'search' => 'Vyhledávání',
'searchName' => 'Podle názvu',
];

View File

@@ -0,0 +1,69 @@
<?php
use KupShop\AdminBundle\AdminList\BaseList;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\RestrictionsBundle\Utils\Restrictions;
/**
* Created by PhpStorm.
* User: hanz
* Date: 3.6.14
* Time: 12:30.
*/
class RestrictionsList extends BaseList
{
protected $tableDef = [
'id' => 'id',
'fields' => [
'Název' => ['field' => 'name'],
'Koho omezíme' => ['field' => 'domain', 'render' => 'renderDomain'],
'Co skryjeme' => ['field' => 'object_value', 'render' => 'renderObject'],
],
];
/**
* @var Restrictions
*/
private $restrictions;
public function __construct()
{
$this->restrictions = ServiceContainer::getService(Restrictions::class);
}
public function renderDomain($values)
{
$domains = array_flip($this->restrictions->getDomainNames());
return translate($domains[$values['domain']], 'restrictions');
}
public function renderObject($values)
{
$objects = array_flip($this->restrictions->getObjectNames());
$objects_names = [];
$object_value = json_decode($values['object_value'], true);
foreach ($object_value as $object => $obj_values) {
$objects_names[] = translate($objects[$object], 'restrictions');
}
return implode(', ', $objects_names);
}
public function getQuery()
{
$qb = sqlQueryBuilder()
->select('id', 'name', 'object_value', 'domain')
->from('restrictions', 'r');
if ($name = getVal('name')) {
$qb->andWhere(\KupShop\CatalogBundle\Query\Search::searchFields($name, [
['field' => 'r.name', 'match' => 'both'],
]));
}
return $qb;
}
}

View File

@@ -0,0 +1,214 @@
<?php
$main_class = 'RestrictionsAdmin';
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\RestrictionsBundle\Utils\Restrictions;
class RestrictionsAdmin extends Window
{
protected $tableName = 'restrictions';
protected $template = 'window/restrictions.tpl';
/**
* @var Restrictions
*/
private $restrictions;
public function __construct()
{
$this->restrictions = ServiceContainer::getService(Restrictions::class);
}
public function get_vars()
{
$vars = parent::get_vars();
$ID = $this->getID();
$pageVars = [];
$data = &$vars['body']['data'];
if (findModule(Modules::PRICE_LEVELS)) {
$pageVars['all_price_levels'] = sqlQueryBuilder()->select('*')
->from('price_levels')
->orderBy('name')
->execute()->fetchAllAssociative();
}
$pageVars['all_user_groups'] = sqlQueryBuilder()->select('*')
->from('users_groups')
->orderBy('name')
->execute()->fetchAllAssociative();
if (!empty($ID)) {
/*
*
* Domain loading
*/
$domains = array_flip($this->restrictions->getDomainNames());
$fields = sqlGetConnection()->getSchemaManager()->listTableColumns('restrictions_domains');
$fields = join(',', array_map(function ($x) {
return 'rdv.'.$x;
}, array_filter(array_keys($fields), function ($x) {
return $x != 'id' && $x != 'id_restriction';
})));
$sql = sqlQuery("SELECT rdv.*, COALESCE({$fields}) AS domain_value, u.name, u.surname, u.email, u.firm
FROM restrictions_domains AS rdv
LEFT JOIN users AS u ON u.id = rdv.id_user
WHERE rdv.id_restriction = :id_restriction", ['id_restriction' => $ID]);
$domain_name = $domains[$data['domain']].'_values';
$pageVars[$domain_name] = [];
foreach ($sql as $value) {
$value['user'] = "{$value['firm']} {$value['name']} {$value['surname']} - {$value['email']}";
$pageVars[$domain_name][$value['domain_value']] = $value;
}
/*
* End domain loading
*
*/
/*
* Object loading
*
*/
$objects_values = $data['object_value'];
if (!empty($objects_values)) {
$objects_values = json_decode($objects_values, true);
}
$objects = array_flip($this->restrictions->getObjectNames());
foreach ($objects_values as $key => $object_values) {
if (is_array($object_values)) {
switch ($key) {
// category
case 1:
$object_values = sqlFetchAll(sqlQuery('SELECT id, name FROM sections WHERE id IN (:values)',
['values' => array_values($object_values)], ['values' => \Doctrine\DBAL\Connection::PARAM_INT_ARRAY]));
break;
// producer
case 2:
$object_values = sqlFetchAll(sqlQuery('SELECT id, name FROM producers WHERE id IN (:values)',
['values' => array_values($object_values)], ['values' => \Doctrine\DBAL\Connection::PARAM_INT_ARRAY]));
break;
// campaign
case 3:
global $cfg;
$loaded_values = [];
foreach ($object_values as &$value) {
$tmp_value['id'] = $value;
$tmp_value['name'] = !empty($cfg['Products']['Flags'][$value]) ? $cfg['Products']['Flags'][$value] : $value;
$loaded_values[$value] = $tmp_value;
}
$object_values = $loaded_values;
break;
// product
case 4:
$object_values = sqlFetchAll(sqlQuery('SELECT id, title AS name FROM products WHERE id IN (:values)',
['values' => array_values($object_values)], ['values' => \Doctrine\DBAL\Connection::PARAM_INT_ARRAY]));
break;
// parameters_list
case 5:
$object_values = array_values($object_values); // preloaded in js
break;
default:
break;
}
$data[$objects[$key].'_values'] = $object_values;
}
}
}
$vars['body']['data'] = array_merge($vars['body']['data'], $pageVars);
$vars['body']['restrictions'] = $this->restrictions;
return $vars;
}
public function getData()
{
$data = parent::getData();
$objects = array_flip($this->restrictions->getObjectNames());
if (getVal('Submit')) {
$data['object_values'] = [];
foreach ($objects as $key => $object) {
if (isset($data[$object.'_values'])) {
$data['object_values'][$key] = [];
foreach ($data[$object.'_values'] as $objects_value) {
$data['object_values'][$key][] = $objects_value;
}
}
}
$data['object_value'] = json_encode($data['object_values']);
$domain_name = array_flip($this->restrictions->getDomainNames())[$data['domain']] ?? '';
$data['domain_invert'] = getVal($domain_name.'_invert') ? 'Y' : 'N';
}
return $data;
}
public function handleUpdate()
{
$SQL = parent::handleUpdate();
$domains = array_flip($this->restrictions->getDomainNames());
$data = parent::getData();
$ID = $this->getID();
$this->deleteSQL('restrictions_domains', ['id_restriction' => $ID]);
if (!empty($data[$domains[$data['domain']].'_values'])) {
foreach ($data[$domains[$data['domain']].'_values'] as $value) {
switch ($data['domain']) {
// country
case 1:
$this->insertSQL('restrictions_domains', ['id_restriction' => $ID, 'country' => $value]);
break;
// user
case 2:
$this->insertSQL('restrictions_domains', ['id_restriction' => $ID, 'id_user' => $value]);
break;
// login
case 4:
$this->insertSQL('restrictions_domains', ['id_restriction' => $ID, 'login' => $value]);
break;
// price level
case 5:
$this->insertSQL('restrictions_domains', ['id_restriction' => $ID, 'id_price_level' => $value]);
break;
// users groups
case 6:
$this->insertSQL('restrictions_domains', ['id_restriction' => $ID, 'id_users_group' => $value]);
break;
// region
case 11:
$this->insertSQL('restrictions_domains', ['id_restriction' => $ID, 'region' => $value]);
break;
}
}
}
return $SQL;
}
}

View File

@@ -0,0 +1,25 @@
{extends file="[shared]/menu.tpl"}
{block name="menu-items"}
<li class="nav-header"><i class="glyphicon {block list_icon}glyphicon-tags{/block}"></i><span>{translate_type type=$type}</li>
<li><a href="javascript:nf('', 'launch.php?s=list.php&amp;type={$type}');"><i class="glyphicon glyphicon-list"></i> <span>{'toolbar_list'|translate}</span></a></li>
<li><a href="javascript:nw('{$type}', '0');"><i class="glyphicon glyphicon-plus"></i> <span>{'toolbar_add'|translate}</span></a></li>
<li class="nav-header smaller"><i class="glyphicon glyphicon-search"></i><span>{'search'|translate}</span></li>
<li class="pill-content">
<ul class="nav-sub nav-pills">
<form id='search' target="mainFrame" method="get" action="launch.php" class="form-inline">
<div class="form-group">
<div class="input-group">
<input type="text" class="form-control input-sm" name="name" value="" placeholder="{'searchName'|translate}"/>
<input type="hidden" name="type" value="restrictions" /><input type="hidden" name="s" value="list.php"/>
<span class="input-group-btn">
<button type="submit" border="0" class="btn btn-primary btn-sm" title="Vyhledat"><i class="glyphicon glyphicon-search"></i></button>
</span>
</div>
</div>
</form>
</ul>
</li>
{/block}

View File

@@ -0,0 +1,176 @@
{extends file="window.tpl"}
{block functions append}
{function name="inversion" field="" checked=""}
<label class="input-group-addon" title="{'inversionInfo'|translate:'filter'}">
<input class="input" type="checkbox" name="{$field}_invert" value="1" {$checked}>
<span class="bi bi-dash-circle-fill"></span>
</label>
{/function}
{/block}
{block title}
Omezení prodeje
{/block}
{block tabs}
{windowTab id='flapRestrictions' label="Omezení prodeje"}
{/block}
{block tabsContent}
<div id="flapRestrictions" class="tab-pane fade active in boxStatic">
<div class="form-group row-flex">
<div class="col-md-3 control-label"><label>{'name'|translate}</label></div>
<div class="col-md-9 control-label">
<input type="text" class="form-control input-sm" name="data[name]" maxlength="100" value="{$body.data.name}" />
</div>
</div>
<h1 class="h4 main-panel-title">{'domain'|translate}</h1>
<div class="form-group">
<div class="col-md-3">
<select name="data[domain]" class="selecter" id="domain">
{foreach $body.restrictions->getDomainNames() as $domain => $key}
<option value="{$key}" {if $body.data.domain == $key}selected{/if} data-domain="{$domain}">podle {$domain|translate|lower}</option>
{/foreach}
</select>
</div>
<div class="col-md-9" id="domain_values">
{foreach $body.restrictions->getDomainNames() as $domain => $key}
{$name = "{$domain}_values"}
<div class="form-group" data-domain="{$domain}" {if $body.data.domain != $key && !(empty($body.data.id) && $key@first)}style="display: none"{/if}>
<div class="col-md-12">
<div class="input-group invert">
<select name="data[{$name}][]" multiple='multiple' class="selecter" id="{$domain}_select"
{if $domain == 'countries' || $domain == 'users_groups' || $domain == 'users'} data-autocomplete="{$domain}" data-preload="{$domain}"{/if}
data-type="{$domain}" data-placeholder="Vyhledejte {$domain|translate|lower} ..">
{if $domain == 'regions'}
{foreach $cfg.Modules.restrictions.regions as $region_name => $id}
<option value="{$id}" {if $body.data[$name][$id]}selected{/if}>{$region_name}</option>
{/foreach}
{elseif $domain == 'countries'}
{foreach $body.data.countries_values as $country}
<option value="{$country.country}" selected></option>
{/foreach}
{elseif $domain == 'price_levels'}
{foreach $body.data.all_price_levels as $pl}
<option value="{$pl.id}" {if $body.data[$name][$pl.id]}selected{/if}>{$pl.name}</option>
{/foreach}
{elseif $domain == 'login'}
<option value="logIn" {if $body.data[$name]['logIn']}selected{/if}>{'login.logIn'|translate:'restrictions'}</option>
<option value="logOut" {if $body.data[$name]['logOut']}selected{/if}>{'login.logOut'|translate:'restrictions'}</option>
{elseif $domain == 'users_groups'}
{foreach $body.data.all_user_groups as $ug}
<option value="{$ug.id}" {if $body.data[$name][$ug.id]}selected{/if}>{$ug.name}</option>
{/foreach}
{else}
{foreach $body.data[$name] as $value}
<option value="{$value.domain_value}" selected> {if !empty($value.id_user)}{$value.user} {else} {$value.domain_value} {/if}</option>
{/foreach}
{/if}
</select>
{inversion field="{$domain}" checked="{if $body.data.domain_invert=='Y'}checked{/if}"}
</div>
</div>
</div>
{/foreach}
</div>
</div>
<h1 class="h4 main-panel-title">Nastavení omezení</h1>
<div class="form-group">
<div class="col-md-3 col-md-offset-3 ">
<div class="h5 no-margin">
<select class="selecter" name="data[type]">
<option value="H" {if $body.data.type == 'H'}selected{/if}>{'products_hide'|translate}</option>
<option value="S" {if $body.data.type == 'S'}selected{/if}>{'products_show'|translate}</option>
</select>
</div>
</div>
</div>
{foreach $body.restrictions->getObjectNames() as $object => $key}
<div class="form-group">
<div class="col-md-3 control-label">
{if $key@first}
<label>patřící do {$object|translate|lower}:</label>
{else}
<label>a zároveň {$object|translate|lower}:</label>
{/if}
</div>
{$name = "{$object}_values"}
{if $object == 'parameters_list'}
{$preload = 'parametersValues'}
{/if}
<div class="col-md-9">
<div>
{if $preload}
<select name="data[{$name}][]" multiple='multiple' class="selecter" data-autocomplete="{$object}"
data-preload="{$preload}" data-placeholder="Vyhledejte {$object|translate|lower}... ">
{foreach $body.data[$name] as $value}
<option value="{$value}" selected> {$value}</option>
{/foreach}
</select>
{else}
<select name="data[{$name}][]" multiple='multiple' class="selecter" id="{$object}_select" data-type="{$object}"
data-placeholder="Vyhledejte {$object|translate|lower}... ">
{if $object == 'campaigns'}
{foreach $cfg.Products.Flags as $char => $flag}
<option value="{$char}" {if $body.data[$name][$char]}selected{/if}>{$flag.plural}</option>
{/foreach}
{else}
{foreach $body.data[$name] as $value}
<option value="{$value.id}" selected> {$value.name}</option>
{/foreach}
{/if}
</select>
{/if}
</div>
</div>
</div>
{/foreach}
<script type="text/javascript">
function AddChanger($name){
$('#'+$name).change( function(){
var $selected = $(this).find('option:selected').data($name);
if ($selected){
$('#'+$name+'_values').find("[data-"+$name+"][data-"+$name+"!='"+$selected+"']").hide().parent().find("[data-"+$name+"='"+$selected+"']").show();
}
}
);
}
AddChanger('domain');
function applyChosen($selector)
{
$selector.ajaxChosen({
dataType: 'json',
type: 'GET',
minTermLength: 0,
url:'launch.php?s=autocomplete.php&type='+$selector.data('type')
},function (data) {
return data;
}, {
width: '100%'
});
}
{foreach $body.restrictions->getDomainNames() as $domain => $id}
{if !in_array($domain, ['regions', 'countries', 'login', 'price_levels', 'users_groups'])}
applyChosen($('#{$domain}_select'));
{/if}
{/foreach}
{foreach $body.restrictions->getObjectNames() as $object => $id}
{if $object != 'campaigns'}
applyChosen($('#{$object}_select'));
{/if}
{/foreach}
</script>
</div>
{/block}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace KupShop\RestrictionsBundle\Entity;
class RestrictionsFilterParams
{
/***
* @param \FilterParams[] $hideFilterParams
* @param \FilterParams[] $showFilterParams
*/
public function __construct(
protected array $hideFilterParams = [],
protected array $showFilterParams = [],
) {
}
/**
* @return \FilterParams[]
*/
public function getHideFilterParams(): array
{
return $this->hideFilterParams;
}
/**
* @return \FilterParams[]
*/
public function getShowFilterParams(): array
{
return $this->showFilterParams;
}
public function isEmpty(): bool
{
return !$this->hideFilterParams && !$this->showFilterParams;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace KupShop\RestrictionsBundle\EventListener;
use KupShop\OrderingBundle\Event\CartEvent;
use KupShop\OrderingBundle\Exception\CartValidationException;
use KupShop\RestrictionsBundle\Utils\Restrictions;
use Query\Operator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class CartItemsListener implements EventSubscriberInterface
{
/**
* @var Restrictions
*/
private $restrictions;
public function __construct(Restrictions $restrictions)
{
$this->restrictions = $restrictions;
}
public static function getSubscribedEvents()
{
return [
CartEvent::CHECK_CART_ITEMS => [
['checkRestrictions', 200],
],
];
}
public function checkRestrictions(CartEvent $event)
{
$cart = $event->getCart();
$cartProducts = array_unique(array_column($cart->products, 'id'));
$checkProducts = sqlQueryBuilder()
->select('p.id')
->from('products', 'p')
->where(Operator::inIntArray($cartProducts, 'p.id'))
->andWhere($this->restrictions->getRestrictionSpec())
->execute()->fetchAll();
$checkProducts = array_column($checkProducts, 'id');
$restrictedProducts = array_diff($cartProducts, $checkProducts);
if ($restrictedProducts) {
$id = reset($restrictedProducts);
$cartItem = array_filter($cart->products, function ($i) use ($id) { return $i['id'] == $id; });
$title = reset($cartItem)['title'];
throw new CartValidationException(sprintf(translate('restrictedProducts', 'ordering'), $title));
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace KupShop\RestrictionsBundle\EventListener;
use KupShop\CatalogBundle\Event\FilterParamsEvent;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\RestrictionsBundle\Utils\Restrictions;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class FilterParamsListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
$events = [];
if (!findModule(\Modules::PRODUCTS_SECTIONS, \Modules::SUB_ELASTICSEARCH)) {
$events[FilterParamsEvent::DEFAULT] = [
['addFilterParam', 200],
];
}
return $events;
}
/**
* @var FilterParamsEvent
*/
public function addFilterParam(FilterParamsEvent $event)
{
$restrictionsSpec = ServiceContainer::getService(Restrictions::class);
$event->addFilterSpec($restrictionsSpec->getRestrictionSpec(), 'restrictions');
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace KupShop\RestrictionsBundle\EventListener;
use KupShop\KupShopBundle\Event\CreateMenuEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class RestrictionsMenuListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
CreateMenuEvent::COMPLETING_TREE => [
['addItem', 200],
],
];
}
/**
* @var CreateMenuEvent
*/
public function addItem(CreateMenuEvent $event)
{
$event->addItem('productsMenu',
[
'name' => 'restrictions',
'left' => 's=menu.php&type=restrictions', 'right' => 's=list.php&type=restrictions',
]
);
$event->addRights('restrictions',
[
'modules' => [
'restrictions',
],
'rights' => ['RESTRICTIONS'],
]
);
}
}

View File

@@ -0,0 +1,7 @@
services:
_defaults:
autowire: true
autoconfigure: true
KupShop\RestrictionsBundle\:
resource: ../../{EventListener,Utils}

View File

@@ -0,0 +1,100 @@
<?php
class UpgradeRestrictions extends UpgradeNew
{
public function check_restrictionsTables()
{
return $this->checkTableExists('restrictions');
}
/** Create restrictions tables**/
public function upgrade_restrictionsTables()
{
sqlQuery('
CREATE TABLE restrictions
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name TEXT DEFAULT NULL,
domain INT NOT NULL,
object_value TEXT NOT NULL
);
CREATE TABLE restrictions_domains
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
id_restriction INT DEFAULT NULL ,
id_user INT(11) UNSIGNED DEFAULT NULL,
country CHAR(2) DEFAULT NULL
);
ALTER TABLE restrictions_domains ADD CONSTRAINT `restrictions_domains_id_restrictions` FOREIGN KEY (id_restriction) REFERENCES restrictions(id) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE restrictions_domains ADD CONSTRAINT `restrictions_domains_id_user` FOREIGN KEY (id_user) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE;
');
}
public function check_restrictionsDomainLogin()
{
return $this->checkColumnExists('restrictions_domains', 'login');
}
/** Add login column to restrictions **/
public function upgrade_restrictionsDomainLogin()
{
sqlQuery('
ALTER TABLE restrictions_domains ADD COLUMN login SET(\'logIn\', \'logOut\') DEFAULT NULL;
');
}
public function check_restrictionsDomainPL()
{
return findModule(Modules::PRICE_LEVELS) && $this->checkColumnExists('restrictions_domains', 'id_price_level');
}
/** Add id_price_level column to restrictions **/
public function upgrade_restrictionsDomainPL()
{
sqlQuery('
ALTER TABLE restrictions_domains ADD COLUMN id_price_level INT UNSIGNED DEFAULT NULL;
');
}
public function check_idUserGroupColumn()
{
return $this->checkColumnExists('restrictions_domains', 'id_users_group');
}
/** Add id_users_group into restrictions_domains */
public function upgrade_idUserGroupColumn()
{
sqlQuery('ALTER TABLE restrictions_domains ADD COLUMN id_users_group INT(11) DEFAULT NULL ');
sqlQuery('ALTER TABLE restrictions_domains ADD CONSTRAINT FK_id_users_group FOREIGN KEY (id_users_group) REFERENCES users_groups(id) ON DELETE CASCADE ON UPDATE CASCADE');
$this->upgradeOK();
}
public function check_domainInvertColumn()
{
return $this->checkColumnExists('restrictions', 'domain_invert');
}
/** Add domain_invert column into restrictions */
public function upgrade_domainInvertColumn()
{
sqlQuery("ALTER TABLE restrictions ADD COLUMN domain_invert ENUM('Y', 'N') NOT NULL DEFAULT 'N' AFTER domain");
$this->upgradeOK();
}
public function check_restrictionTypeColumn()
{
return $this->checkColumnExists('restrictions', 'type');
}
/** Restrictions - add type column into restrictions table */
public function upgrade_restrictionTypeColumn()
{
sqlQuery("alter table restrictions add type ENUM ('H', 'S') default 'H' not null comment '(H)ide, (S)how';");
$this->upgradeOK();
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace KupShop\RestrictionsBundle;
use KupShop\KupShopBundle\Config;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class RestrictionsBundle extends Bundle
{
public function boot()
{
parent::boot();
$cfg = &Config::get()->getContainer();
// B2B modul to zapina, ale je potreba to jeste pridat do cfg
if (empty($cfg['Modules']['restrictions'])) {
$cfg['Modules']['restrictions'] = true;
}
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace KupShop\RestrictionsBundle\Tests;
use KupShop\DevelopmentBundle\Util\Tests\CartTestTrait;
use KupShop\RestrictionsBundle\Utils\Restrictions;
class RestrictionsProductListTest extends \DatabaseTestCase
{
use CartTestTrait;
/** @var ProductList */
private $productList;
/** @var Restrictions */
private $restrictions;
protected function setUp(): void
{
parent::setUp();
$this->productList = new \ProductList();
$this->restrictions = new Restrictions();
}
/**
* @dataProvider dataSet_testProductListRestricts
*
* @param int $domainId
* @param int $expectedCount
*/
public function testProductListRestricts($domainId, $expectedCount)
{
$this->prepareDomain($domainId);
$this->productList->applyDefaultFilterParams();
$products = $this->productList->getProducts();
$this->assertCount($expectedCount, $products);
}
public function dataSet_testProductListRestricts()
{
return [
'exclude category' => [1, 8],
'exclude producer' => [2, 9],
'exclude campaign' => [3, 7],
'exclude product' => [4, 6],
'exclude product in campaign' => [6, 9],
'empty restriction' => [7, 11],
];
}
public function testThrowsOnInvalidJson()
{
$this->insertSQL('restrictions', [
'domain' => 1, // countries
'object_value' => 'invalid json should throw',
]);
$this->prepareDomain(sqlGetConnection()->lastInsertId());
$this->expectException('JsonException');
$this->productList->applyDefaultFilterParams();
$this->productList->getProducts();
}
public function testGetRestrictionSpec()
{
$this->prepareDomain(4);
$specs = $this->restrictions->getRestrictionSpec();
$this->assertInstanceOf('Closure', $specs);
}
public function testGetRestrictionSpecSql()
{
$this->prepareDomain(1);
$snippet = $this->restrictions->getRestrictionSpecSnippet();
$this->assertRegExp('/NOT \(EXISTS \(.+\)/', $snippet->where);
$this->assertCount(1, $snippet->types);
}
private function prepareDomain($domainId)
{
$this->insertSQL('restrictions_domains', [
'id_restriction' => $domainId,
'country' => 'CZ',
]);
$this->loginUser(1);
}
public function getDataSet()
{
$objects = findModule('restrictions', 'objects');
$domains = findModule('restrictions', 'domains');
// Delete autogenerated product trash-cart
$this->deleteSQL('products', ['id' => 0]);
$arr = [
'users' => [
[
'id' => 1,
'country' => 'CZ',
],
],
'restrictions' => [
[
'id' => 1,
'domain' => $domains['countries'],
'object_value' => json_encode([$objects['categories'] => [3]]),
],
[
'id' => 2,
'domain' => $domains['countries'],
'object_value' => json_encode([$objects['producers'] => [10, 12]]),
],
[
'id' => 3,
'domain' => $domains['countries'],
'object_value' => json_encode([$objects['campaigns'] => ['N']]),
],
[
'id' => 4,
'domain' => $domains['countries'],
'object_value' => json_encode([$objects['products'] => [1, 2, 3, 4, 5]]),
],
[
'id' => 5,
'domain' => $domains['countries'],
'object_value' => json_encode([$objects['products'] => [6, 7]]),
],
[
'id' => 6,
'domain' => $domains['countries'],
'object_value' => json_encode([
$objects['products'] => [8, 11],
$objects['campaigns'] => ['N'],
]),
],
[
'id' => 7,
'domain' => $domains['countries'],
'object_value' => '[]',
],
],
];
return $this->getJsonDataSet(json_encode($arr));
}
}

View File

@@ -0,0 +1,134 @@
{
"sections": [
{
"id": 1,
"id_block": null,
"name": "Kategorie s produktama",
"name_short": "Kategorie s produktama",
"figure": "Y",
"priority": 0,
"behaviour": "2",
"orderby": "title",
"orderdir": "ASC",
"date_updated": "2025-03-28 13:47:07",
"list_variations": "N",
"lead_figure": "N",
"lead_text": "",
"lead_products": "",
"photo": null,
"meta_title": "",
"meta_description": "",
"meta_keywords": null,
"feed_heureka": null,
"feed_heureka_sk": null,
"feed_google": null,
"feed_glami": null,
"flags": "",
"feed_seznam": null,
"producers_filter": "N",
"url": null,
"redirect_url": null,
"data": "{\"enable_sort_by_in_store\": \"Y\", \"enable_sort_by_product_position\": \"Y\", \"filters_inherit_settings\": \"N\"}",
"template": "",
"virtual": "N",
"filter_url": null,
"title": null,
"producers_indexing": "N",
"producers_to_title": "N",
"show_in_search": "Y",
"labels_filter": "N"
},
{
"id": 2,
"id_block": null,
"name": "Kategorie bez produktu 1",
"name_short": "Kategorie bez produktu 1",
"figure": "Y",
"priority": 0,
"behaviour": "2",
"orderby": "title",
"orderdir": "ASC",
"date_updated": "2025-03-28 13:47:22",
"list_variations": "N",
"lead_figure": "N",
"lead_text": "",
"lead_products": "",
"photo": null,
"meta_title": "",
"meta_description": "",
"meta_keywords": null,
"feed_heureka": null,
"feed_heureka_sk": null,
"feed_google": null,
"feed_glami": null,
"flags": "",
"feed_seznam": null,
"annotation": null,
"producers_filter": "N",
"url": null,
"redirect_url": null,
"data": null,
"template": "",
"virtual": "N",
"filter_url": null,
"title": null,
"producers_indexing": "N",
"producers_to_title": "N",
"show_in_search": "Y",
"labels_filter": "N"
},
{
"id": 3,
"id_block": null,
"name": "Kategorie bez produktu 2",
"name_short": "Kategorie bez produktu 2",
"figure": "Y",
"priority": 0,
"behaviour": "2",
"orderby": "title",
"orderdir": "ASC",
"date_updated": "2025-03-28 13:46:58",
"list_variations": "N",
"lead_figure": "N",
"lead_text": "",
"lead_products": "",
"photo": null,
"meta_title": "",
"meta_description": "",
"meta_keywords": null,
"feed_heureka": null,
"feed_heureka_sk": null,
"feed_google": null,
"feed_glami": null,
"flags": "",
"feed_seznam": null,
"annotation": null,
"producers_filter": "N",
"url": null,
"redirect_url": null,
"data": null,
"template": "",
"virtual": "N",
"filter_url": null,
"title": null,
"producers_indexing": "N",
"producers_to_title": "N",
"show_in_search": "Y",
"labels_filter": "Y"
}
],
"products_in_sections": [
{
"id_product": 1,
"id_section": 1,
"figure": "Y",
"generated": 0
},
{
"id_product": 2,
"id_section": 1,
"figure": "Y",
"generated": 0
}
]
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace KupShop\RestrictionsBundle\Tests;
use KupShop\CatalogBundle\Section\SectionTree;
use KupShop\RestrictionsBundle\Utils\RestrictionUtil;
class RestrictionsSectionsTest extends \DatabaseTestCase
{
private RestrictionUtil $restrictionUtil;
private SectionTree $sectionTree;
protected function setUp(): void
{
parent::setUp();
$this->restrictionUtil = $this->get(RestrictionUtil::class);
$this->sectionTree = $this->get(SectionTree::class);
}
/** @dataProvider data_testRestrictSections */
public function testRestrictSections(string $showEmptySections, int $expectedCount): void
{
\Settings::getDefault()->cat_show_empty = $showEmptySections;
$sections = $this->restrictionUtil->restrictSections(
$this->sectionTree->getTree()
);
$this->assertCount($expectedCount, $sections);
}
public function data_testRestrictSections(): iterable
{
yield 'Show empty sections is disabled' => ['N', 1];
yield 'Show empty sections is enabled' => ['Y', 3];
}
protected function getDataSet()
{
return $this->getJsonDataSetFromFile();
}
}

View File

@@ -0,0 +1,244 @@
<?php
namespace KupShop\RestrictionsBundle\Tests;
use KupShop\ContentBundle\View\ProductView;
use KupShop\DevelopmentBundle\Util\Tests\CartTestTrait;
use KupShop\RestrictionsBundle\Utils\Restrictions;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class RestrictionsTest extends \DatabaseTestCase
{
use CartTestTrait;
/** @var Restrictions */
private $restrictions;
/** @var ProductView */
private $productView;
protected function setUp(): void
{
parent::setUp();
$this->restrictions = $this->get(Restrictions::class);
$this->productView = $this->get(ProductView::class);
}
public function getDataSet()
{
// Omezení Nepřihlášený uživatel - restricted product ID=4
// Omezení wpj (id_user = 1,2,3) - restricted product ID=9
// Omezení wpjwpj (id_user != 2) - restricted product ID=11
return $this->getJsonDataSet('
{
"restrictions": [
{
"id":3,
"name":"Omezení Nepřihlášený uživatel",
"domain":4,
"domain_invert":"N",
"object_value":"{\"4\":[\"4\"]}"
},
{
"id":5,
"name":"Omezení wpj",
"domain":2,
"domain_invert":"N",
"object_value":"{\"4\":[\"9\"]}"
},
{
"id":6,
"name":"Omezení wpjwpj",
"domain":2,
"domain_invert":"Y",
"object_value":"{\"4\":[\"11\"]}"
}
],
"restrictions_domains": [
{
"id":210,
"id_restriction":3,
"id_user":null,
"country":null,
"login":"logOut",
"id_price_level":null,
"id_users_group":null
},
{
"id":240,
"id_restriction":5,
"id_user":1,
"country":null,
"login":null,
"id_price_level":null,
"id_users_group":null
},
{
"id":241,
"id_restriction":5,
"id_user":2,
"country":null,
"login":null,
"id_price_level":null,
"id_users_group":null
},
{
"id":242,
"id_restriction":5,
"id_user":3,
"country":null,
"login":null,
"id_price_level":null,
"id_users_group":null
},
{
"id":243,
"id_restriction":6,
"id_user":2,
"country":null,
"login":null,
"id_price_level":null,
"id_users_group":null
}
]
}
');
}
/** @dataProvider data_testVisibleProduct1 */
public function testVisibleProduct1($id_user, $restricted)
{
if ($id_user) {
$this->loginUser($id_user);
}
$this->productView->setProductId(1);
if ($restricted) {
$this->expectException(NotFoundHttpException::class);
$this->expectExceptionMessage('Product not found');
$this->productView->checkProductExist();
} else {
$product = $this->productView->getProduct();
$this->assertNotFalse($product);
$this->assertInstanceOf(\Product::class, $product);
$this->assertContains('NIKE Capri LACE Test Dlouheho Nazvu Produktu Test Dlouheho Produktu Tes', $product->title);
}
}
public function data_testVisibleProduct1()
{
// product ID=1 is visible for everyone
return [
[1, false],
[2, false],
[3, false],
[null, false],
];
}
/** @dataProvider data_RestrictedProduct4 */
public function testRestrictedProduct4($id_user, $restricted)
{
if ($id_user) {
$this->loginUser($id_user);
}
$this->productView->setProductId(4);
if ($restricted) {
$this->expectException(NotFoundHttpException::class);
$this->expectExceptionMessage('Product not found');
$this->productView->checkProductExist();
} else {
$product = $this->productView->getProduct();
$this->assertNotFalse($product);
$this->assertInstanceOf(\Product::class, $product);
$this->assertContains('Columbia Lay D Down', $product->title);
}
}
public function data_RestrictedProduct4()
{
// Omezení Nepřihlášený uživatel:
// product ID=4 is visible for logged in users only
return [
[1, false],
[2, false],
[3, false],
[null, true],
];
}
/** @dataProvider data_RestrictedProduct9 */
public function testRestrictedProduct9($id_user, $restricted)
{
if ($id_user) {
$this->loginUser($id_user);
}
$this->productView->setProductId(9);
if ($restricted) {
$this->expectException(NotFoundHttpException::class);
$this->expectExceptionMessage('Product not found');
$this->productView->checkProductExist();
} else {
$product = $this->productView->getProduct();
$this->assertNotFalse($product);
$this->assertInstanceOf(\Product::class, $product);
$this->assertContains('DeWALT DCD780C2 aku vrtačka', $product->title);
}
}
public function data_RestrictedProduct9()
{
// Omezení wpj (id_user = 1,2,3):
// product ID=9 is not visible for wpj users
return [
[1, true],
[2, true],
[3, true],
[null, false],
];
}
/** @dataProvider data_RestrictedProduct11 */
public function testRestrictedProduct11($id_user, $restricted)
{
if ($id_user) {
$this->loginUser($id_user);
}
$this->productView->setProductId(11);
if ($restricted) {
$this->expectException(NotFoundHttpException::class);
$this->expectExceptionMessage('Product not found');
$this->productView->checkProductExist();
} else {
$product = $this->productView->getProduct();
$this->assertNotFalse($product);
$this->assertInstanceOf(\Product::class, $product);
$this->assertContains('iPhone wpj', $product->title);
}
}
public function data_RestrictedProduct11()
{
// Omezení wpjwpj (id_user != 2):
// product ID=11 is visible for user wpj@wpj.cz only
return [
[1, true],
[2, false],
[3, true],
[null, true],
];
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace KupShop\RestrictionsBundle\Utils;
use KupShop\CatalogBundle\Entity\Section;
use KupShop\CatalogBundle\ProductList\FilterParams;
use KupShop\CatalogBundle\Util\SectionUtil;
class RestrictionUtil
{
public function __construct(
private readonly FilterParams $filterParams,
private readonly Restrictions $restrictions,
) {
}
/**
* @param Section[] $sections
*
* @return Section[]
*/
public function restrictSections(array $sections): array
{
$dbcfg = \Settings::getDefault();
// show empty sections is enabled, so we ignore restrictions because all sections should be visible, even if they are empty
if ($dbcfg->cat_show_empty === 'Y') {
return $sections;
}
$qb = sqlQueryBuilder()
->select('DISTINCT ps.id_section as id')
->fromProducts()
->joinSectionsOnProducts();
$qb->andWhere($this->restrictions->getRestrictionSpec());
$qb->andWhere($this->filterParams->getSpec());
$sectionIds = sqlFetchAll($qb, 'id');
SectionUtil::recurseDeleteSections($sections, $sectionIds);
return $sections;
}
}

View File

@@ -0,0 +1,367 @@
<?php
namespace KupShop\RestrictionsBundle\Utils;
use KupShop\RestrictionsBundle\Entity\RestrictionsFilterParams;
use Query\Filter;
use Query\Operator as Op;
use Query\Product;
class Restrictions
{
/** @var array */
private $restrictionSpec;
/** @var \Raven_Client */
private $raven;
public const TYPE_HIDE = 'H';
public const TYPE_SHOW = 'S';
protected $cfg = [
'Modules' => [
'restrictions' => [
'objects' => [
'categories' => 1,
'producers' => 2,
'campaigns' => 3,
'products' => 4,
'parameters_list' => 5,
],
'domains' => [
'countries' => 1,
'users' => 2,
'login' => 4,
'price_levels' => 5,
'users_groups' => 6,
],
],
],
];
public function getRestrictionSpec()
{
// v adminu - v detailu objednavky ignorujeme restrictions
if (isAdministration()) {
return null;
}
if ($this->restrictionSpec === null) {
$this->restrictionSpec = $this->createSpec();
}
return $this->restrictionSpec;
}
/**
* @return \Query
*/
public function getRestrictionSpecSnippet()
{
$qb = sqlQueryBuilder();
$query = new \Query();
$query->where = (string) $qb->evaluateClosures([$this->getRestrictionSpec()])[0];
$query->data = $qb->getParameters();
$query->types = $qb->getParameterTypes();
return $query;
}
/**
* @return array
*/
public function getDbQuery()
{
$qb = sqlQueryBuilder()->select('r.id, (r.domain_invert = "Y") AS invert, r.object_value, r.type')
->from('restrictions', 'r')
->join('r', 'restrictions_domains', 'rd', 'r.id = rd.id_restriction')
->groupBy('r.id');
$find = [];
$user = \User::getCurrentUser();
$find[] = 'WHEN 4 THEN FIND_IN_SET(:login, GROUP_CONCAT(rd.login))';
$qb->addParameters(['login' => ($user ? 'logIn' : 'logOut')]);
// Při registraci v košíku Future uživatel neexistuje a nemáme o něm informace a proto id > 0
if ($user && $user->id > 0) {
$find[] = 'WHEN 1 THEN FIND_IN_SET(:country, GROUP_CONCAT(rd.country))';
$qb->addParameters(['country' => $user->country]);
$find[] = 'WHEN 2 THEN FIND_IN_SET(:user, GROUP_CONCAT(rd.id_user))';
$qb->addParameters(['user' => $user->id]);
if (findModule(\Modules::PRICE_LEVELS) && $user->getUserPriceLevelId()) {
$find[] = 'WHEN 5 THEN FIND_IN_SET(:price_level, GROUP_CONCAT(rd.id_price_level))';
$qb->addParameters(['price_level' => $user->getUserPriceLevelId()]);
}
if ($user->getGroups()) {
$find_groups = [];
foreach ($user->getGroups() as $group) {
$find_groups[] = "FIND_IN_SET(:group{$group['id']}, GROUP_CONCAT(rd.id_users_group))";
$qb->addParameters(["group{$group['id']}" => $group['id']]);
}
$find_groups = implode('+', $find_groups);
$find[] = "WHEN 6 THEN {$find_groups}";
}
}
return [$qb, $find];
}
private function getDbRules(): array
{
[$qb, $find] = $this->getDbQuery();
$cacheKey = $this->getCacheKey($qb->getParameters());
if ($cachedRules = getCache($cacheKey)) {
return $cachedRules;
}
$find = '(CASE r.domain
'.implode('
', $find).'
ELSE 0
END) AS find';
$qb->addSelect($find);
$qb->having('(invert XOR find)');
$ruleRows = $qb->execute()->fetchAllAssociative();
setCache($cacheKey, $ruleRows, 30 * 60);
return $ruleRows;
}
/**
* @param array $specs default implicit specs
*
* @return callable
*/
protected function createSpec($specs = [])
{
$ruleRows = $this->getDbRules();
$typeSpecs = [];
foreach ($ruleRows as $ruleRow) {
$ruleArrays = json_decode_strict($ruleRow['object_value'], true);
$rowSpecs = $this->getRuleRowSpecs($ruleArrays);
if (!$rowSpecs) {
continue;
}
switch ($ruleRow['type']) {
case self::TYPE_HIDE:
$typeSpecs[self::TYPE_HIDE][] = Op::not(Op::andX($rowSpecs));
break;
case self::TYPE_SHOW:
$typeSpecs[self::TYPE_SHOW][] = Op::andX($rowSpecs);
break;
}
}
if ($typeSpecs[self::TYPE_HIDE] ?? false) {
$specs[] = Op::andX($typeSpecs[self::TYPE_HIDE]);
}
if ($typeSpecs[self::TYPE_SHOW] ?? false) {
$specs[] = Op::orX($typeSpecs[self::TYPE_SHOW]);
}
if ($specs) {
return Op::andX($specs);
}
return function () {
return '1';
};
}
private function getRuleRowSpecs($ruleArrays)
{
$rowSpecs = [];
$objectNames = array_flip($this->getObjectNames());
foreach ($ruleArrays as $objectId => $ruleArray) {
$objectName = $objectNames[$objectId];
$ruleArray = array_values($ruleArray);
if (empty($ruleArray)) {
// skip empty rules (when restrictions are incorrectly imported)
continue;
}
$specName = 'restrict_'.$objectName;
if (method_exists($this, $specName)) {
$rowSpecs[] = function () use ($specName, $ruleArray) {
return call_user_func([$this, $specName], $ruleArray);
};
} else {
$this->getRaven()->captureMessage(
"No spec found to restrict results by '%s'.",
[$objectName]
);
}
}
return $rowSpecs;
}
public function getRestrictionFilterParams(): RestrictionsFilterParams
{
$ruleRows = $this->getDbRules();
$filterParamsByType = [];
foreach ($ruleRows as $ruleRow) {
$ruleArrays = json_decode_strict($ruleRow['object_value'], 1);
$filterParams = $this->getRuleRowFilterParams($ruleArrays);
if (!$filterParams) {
continue;
}
$filterParamsByType[$ruleRow['type']][] = $filterParams;
}
return new RestrictionsFilterParams($filterParamsByType[self::TYPE_HIDE] ?? [], $filterParamsByType[self::TYPE_SHOW] ?? []);
}
private function getRuleRowFilterParams($ruleArrays)
{
$filterParams = new \FilterParams();
$objectNames = array_flip($this->getObjectNames());
foreach ($ruleArrays as $objectId => $ruleArray) {
$objectName = $objectNames[$objectId];
$ruleArray = array_values($ruleArray);
switch ($objectName) {
case 'categories':
$filterParams->setSections($ruleArray);
break;
case 'producers':
$filterParams->setProducers($ruleArray);
break;
case 'campaigns':
foreach ($ruleArray as $campaignId) {
$filterParams->setCampaign($campaignId);
}
break;
case 'products':
$filterParams->setProducts($ruleArray);
break;
case 'parameters_list':
$paramsListParamIds = sqlQueryBuilder()->select('id, id_parameter')->from('parameters_list')
->where(Op::inIntArray($ruleArray, 'id'));
$paramsIndex = [];
foreach ($paramsListParamIds->execute() as $param) {
$paramsIndex[$param['id_parameter']][] = $param['id'];
}
foreach ($paramsIndex as $paramId => $paramValues) {
$filterParams->setParameter($paramId, $paramValues);
}
break;
}
}
return $filterParams;
}
public function getObjectNames()
{
return array_merge($this->cfg['Modules']['restrictions']['objects'], findModule('restrictions', 'objects', []));
}
public function getDomainNames()
{
return array_merge($this->cfg['Modules']['restrictions']['domains'], findModule('restrictions', 'domains', []));
}
protected function restrict_categories(array $categoryIds)
{
return Product::inSections($categoryIds, 'rps');
}
protected function restrict_producers(array $producerIds)
{
return Op::andX(
Filter::byProducers($producerIds),
'pr.id is not null'
);
}
protected function restrict_campaigns(array $campaigns)
{
return Filter::byCampaigns($campaigns, 'OR');
}
protected function restrict_products(array $productIds)
{
return Op::inIntArray($productIds, 'p.id');
}
protected function restrict_parameters_list(array $parameterIds)
{
$filterCallback = function ($alias, $paramId, $paramData) {
return Op::equals(["{$alias}.value_list" => $paramData]);
};
$parameters = array_combine($parameterIds, $parameterIds);
return Filter::parametersFilterUtil($parameters, $filterCallback, false);
}
/**
* @return \Raven_Client
*/
public function getRaven()
{
if (!$this->raven) {
$this->raven = getRaven();
}
return $this->raven;
}
public function setRaven(\Raven_Client $raven)
{
$this->raven = $raven;
}
/**
* @param $query array containing at least 'data', 'where' and 'types' keys
*
* @return array modified query
*/
public function applyToOldQuery($query)
{
$restrictions = $this->getRestrictionSpecSnippet();
$query['data'] = array_merge(
$query['data'],
$restrictions->data
);
$query['types'] = array_merge(
$query['types'],
$restrictions->types
);
$query['where'] .= " AND ({$restrictions->where})";
return $query;
}
private function getCacheKey(array $parameters): string
{
$cacheKey = '';
foreach ($parameters as $key => $value) {
$cacheKey .= '_'.$key.'-'.$value;
}
return 'restrictions'.$cacheKey;
}
}