@@ -41,6 +42,12 @@ $current_page = sanitize_text_field($_GET['page'] ?? 'sodino-upsells');
+
+
+
+
+
+
diff --git a/app/Controllers/AdminController.php b/app/Controllers/AdminController.php
index 613dae2..f753c16 100644
--- a/app/Controllers/AdminController.php
+++ b/app/Controllers/AdminController.php
@@ -15,6 +15,16 @@ class AdminController {
private $ruleRepository;
private $upsellRepository;
private $bannerRepository;
+ private $allowedBannerContentTypes = ['image', 'html', 'shortcode'];
+ private $allowedBannerPositions = ['top', 'middle', 'bottom', 'product_page', 'cart'];
+ private $allowedBannerDisplayTypes = ['inline', 'popup', 'floating_bar'];
+ private $allowedBannerUserTargets = ['all', 'new', 'returning'];
+ private $allowedBannerDeviceTargets = ['all', 'desktop', 'mobile'];
+ private $allowedUpsellTriggerTypes = ['product', 'category', 'cart_total'];
+ private $allowedUpsellDiscountTypes = ['percentage', 'fixed', 'none'];
+ private $allowedRuleConditionTypes = ['user_type', 'product_category', 'product_ids', 'cart_total_min', 'cart_total_max', 'cart_item_count_min', 'cart_item_count_max'];
+ private $allowedRuleActionTypes = ['discount_percent', 'discount_fixed', 'set_price', 'free_shipping'];
+ private $allowedStrategies = ['priority', 'highest_discount', 'first_valid'];
public function __construct(RuleRepository $ruleRepository, UpsellRepository $upsellRepository, BannerRepository $bannerRepository) {
$this->ruleRepository = $ruleRepository;
@@ -39,7 +49,7 @@ class AdminController {
add_submenu_page(
'sodino-rules',
__('قوانین قیمتگذاری', 'sodino'),
- __('قوانین قیمتگذاری', 'sودino'),
+ __('قوانین قیمتگذاری', 'sodino'),
'manage_options',
'sodino-rules',
[$this, 'rulesPage']
@@ -47,8 +57,8 @@ class AdminController {
add_submenu_page(
'sodino-rules',
- __('افزودن قانون', 'sودino'),
- __('افزودن قانون', 'sودino'),
+ __('افزودن قانون', 'sodino'),
+ __('افزودن قانون', 'sodino'),
'manage_options',
'sodino-add-rule',
[$this, 'addRulePage']
@@ -101,7 +111,7 @@ class AdminController {
add_submenu_page(
'sodino-rules',
- __('داشبورد سودینو', 'sودino'),
+ __('داشبورد سودینو', 'sodino'),
__('داشبورد سودینو', 'sodino'),
'manage_options',
'sodino-dashboard',
@@ -110,14 +120,61 @@ class AdminController {
add_submenu_page(
'sodino-rules',
- __('تنظیمات', 'sودino'),
- __('تنظیمات', 'sودینو'),
+ __('تنظیمات', 'sodino'),
+ __('تنظیمات', 'sodino'),
'manage_options',
'sodino-settings',
[$this, 'settingsPage']
);
}
+ private function redirectWithNotice($url, $message, $type = 'error') {
+ $notice = [
+ 'type' => $type,
+ 'message' => $message,
+ ];
+
+ set_transient('sodino_admin_notice_' . get_current_user_id(), $notice, 60);
+ set_transient('sodino_admin_notice', $notice, 60);
+
+ if ($type === 'error' && $_SERVER['REQUEST_METHOD'] === 'POST') {
+ $oldInput = wp_unslash($_POST);
+ $oldInput['_sodino_page'] = isset($_GET['page']) ? sanitize_key($_GET['page']) : '';
+ set_transient('sodino_old_input_' . get_current_user_id(), $oldInput, 120);
+ } else {
+ delete_transient('sodino_old_input_' . get_current_user_id());
+ }
+
+ wp_safe_redirect($url);
+ exit;
+ }
+
+ private function getBackUrl($fallbackPage) {
+ return wp_get_referer() ?: admin_url('admin.php?page=' . $fallbackPage);
+ }
+
+ private function requireValue($value, $message, $fallbackPage) {
+ if (is_string($value)) {
+ $value = trim($value);
+ }
+
+ if ($value === '' || $value === null) {
+ $this->redirectWithNotice($this->getBackUrl($fallbackPage), $message, 'error');
+ }
+
+ return $value;
+ }
+
+ private function normalizeDatetime($value) {
+ $value = trim((string) $value);
+ if ($value === '') {
+ return null;
+ }
+
+ $timestamp = strtotime($value);
+ return $timestamp ? date('Y-m-d H:i:s', $timestamp) : null;
+ }
+
/**
* Rules admin page
*/
@@ -289,6 +346,10 @@ class AdminController {
}
private function saveUpsell($upsell = null) {
+ if (!current_user_can('manage_options')) {
+ wp_die(__('دسترسی کافی ندارید.', 'sodino'));
+ }
+
if (!isset($_POST['sodino_upsell_nonce']) || !wp_verify_nonce($_POST['sodino_upsell_nonce'], 'sodino_save_upsell')) {
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
}
@@ -297,22 +358,49 @@ class AdminController {
$upsell = new Upsell();
}
- $upsell->title = sanitize_text_field($_POST['title'] ?? '');
- $upsell->trigger_type = sanitize_text_field($_POST['trigger_type'] ?? 'product');
- $upsell->trigger_value = sanitize_text_field($_POST['trigger_value'] ?? '');
+ $triggerType = sanitize_key($_POST['trigger_type'] ?? 'product');
+ if (!in_array($triggerType, $this->allowedUpsellTriggerTypes, true)) {
+ $triggerType = 'product';
+ }
+
+ $triggerValues = isset($_POST['trigger_values']) && is_array($_POST['trigger_values']) ? wp_unslash($_POST['trigger_values']) : [];
+ $triggerValue = sanitize_text_field($triggerValues[$triggerType] ?? '');
+ $targetProductId = max(0, intval($_POST['target_product_id'] ?? 0));
+ $discountType = sanitize_key($_POST['discount_type'] ?? 'percentage');
+ if (!in_array($discountType, $this->allowedUpsellDiscountTypes, true)) {
+ $discountType = 'percentage';
+ }
+
+ $title = sanitize_text_field($_POST['title'] ?? '');
+ $this->requireValue($title, __('عنوان آپسل الزامی است.', 'sodino'), 'sodino-add-upsell');
+ $this->requireValue($triggerValue, __('مقدار شرط آپسل را انتخاب یا وارد کنید.', 'sodino'), 'sodino-add-upsell');
+
+ if ($targetProductId <= 0) {
+ $this->redirectWithNotice($this->getBackUrl('sodino-add-upsell'), __('محصول پیشنهادی آپسل الزامی است.', 'sodino'), 'error');
+ }
+
+ $upsell->title = $title;
+ $upsell->trigger_type = $triggerType;
+ $upsell->trigger_value = $triggerValue;
$upsell->target_product_id = max(0, intval($_POST['target_product_id'] ?? 0));
- $upsell->discount_type = sanitize_text_field($_POST['discount_type'] ?? 'percentage');
+ $upsell->discount_type = $discountType;
$upsell->discount_value = max(0, floatval($_POST['discount_value'] ?? 0));
$upsell->priority = max(1, intval($_POST['priority'] ?? 10));
$upsell->status = isset($_POST['status']) ? 1 : 0;
- $this->upsellRepository->save($upsell);
+ $id = $this->upsellRepository->save($upsell);
+ if (!$id) {
+ $this->redirectWithNotice($this->getBackUrl('sodino-add-upsell'), __('ذخیره آپسل انجام نشد. لطفا مقادیر فرم را بررسی کنید.', 'sodino'), 'error');
+ }
- wp_safe_redirect(admin_url('admin.php?page=sodino-upsells'));
- exit;
+ $this->redirectWithNotice(admin_url('admin.php?page=sodino-upsells'), __('آپسل با موفقیت ذخیره شد.', 'sodino'), 'success');
}
private function saveBanner($banner = null) {
+ if (!current_user_can('manage_options')) {
+ wp_die(__('دسترسی کافی ندارید.', 'sodino'));
+ }
+
if (!isset($_POST['sodino_banner_nonce']) || !wp_verify_nonce($_POST['sodino_banner_nonce'], 'sodino_save_banner')) {
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
}
@@ -321,26 +409,60 @@ class AdminController {
$banner = new Banner();
}
- $banner->title = sanitize_text_field($_POST['title'] ?? '');
- $banner->content_type = sanitize_text_field($_POST['content_type'] ?? 'image');
- $banner->content_value = isset($_POST['content_value']) ? wp_kses_post($_POST['content_value']) : '';
+ $contentType = sanitize_key($_POST['content_type'] ?? 'image');
+ if (!in_array($contentType, $this->allowedBannerContentTypes, true)) {
+ $contentType = 'image';
+ }
+
+ $contentValues = isset($_POST['content_values']) && is_array($_POST['content_values']) ? wp_unslash($_POST['content_values']) : [];
+ $contentValue = $contentValues[$contentType] ?? '';
+ $contentValue = $contentType === 'image' ? esc_url_raw($contentValue) : wp_kses_post($contentValue);
+
+ $title = sanitize_text_field($_POST['title'] ?? '');
+ $this->requireValue($title, __('عنوان بنر الزامی است.', 'sodino'), 'sodino-add-banner');
+ $this->requireValue($contentValue, __('محتوای بنر الزامی است.', 'sodino'), 'sodino-add-banner');
+
+ if ($contentType === 'image' && !filter_var($contentValue, FILTER_VALIDATE_URL)) {
+ $this->redirectWithNotice($this->getBackUrl('sodino-add-banner'), __('آدرس تصویر بنر معتبر نیست.', 'sodino'), 'error');
+ }
+
+ $startTime = $this->normalizeDatetime($_POST['start_time'] ?? '');
+ $endTime = $this->normalizeDatetime($_POST['end_time'] ?? '');
+ if ($startTime && $endTime && strtotime($endTime) < strtotime($startTime)) {
+ $this->redirectWithNotice($this->getBackUrl('sodino-add-banner'), __('تاریخ پایان بنر نباید قبل از تاریخ شروع باشد.', 'sodino'), 'error');
+ }
+
+ $position = sanitize_key($_POST['position'] ?? 'top');
+ $displayType = sanitize_key($_POST['display_type'] ?? 'inline');
+ $userTarget = sanitize_key($_POST['user_target'] ?? 'all');
+ $deviceTarget = sanitize_key($_POST['device_target'] ?? 'all');
+
+ $banner->title = $title;
+ $banner->content_type = $contentType;
+ $banner->content_value = $contentValue;
$banner->link_url = esc_url_raw($_POST['link_url'] ?? '');
- $banner->position = sanitize_text_field($_POST['position'] ?? 'top');
- $banner->display_type = sanitize_text_field($_POST['display_type'] ?? 'inline');
- $banner->start_time = sanitize_text_field($_POST['start_time'] ?? '');
- $banner->end_time = sanitize_text_field($_POST['end_time'] ?? '');
- $banner->user_target = sanitize_text_field($_POST['user_target'] ?? 'all');
- $banner->device_target = sanitize_text_field($_POST['device_target'] ?? 'all');
+ $banner->position = in_array($position, $this->allowedBannerPositions, true) ? $position : 'top';
+ $banner->display_type = in_array($displayType, $this->allowedBannerDisplayTypes, true) ? $displayType : 'inline';
+ $banner->start_time = $startTime;
+ $banner->end_time = $endTime;
+ $banner->user_target = in_array($userTarget, $this->allowedBannerUserTargets, true) ? $userTarget : 'all';
+ $banner->device_target = in_array($deviceTarget, $this->allowedBannerDeviceTargets, true) ? $deviceTarget : 'all';
$banner->priority = max(1, intval($_POST['priority'] ?? 10));
$banner->status = isset($_POST['status']) ? 1 : 0;
- $this->bannerRepository->save($banner);
+ $id = $this->bannerRepository->save($banner);
+ if (!$id) {
+ $this->redirectWithNotice($this->getBackUrl('sodino-add-banner'), __('ذخیره بنر انجام نشد. لطفا مقادیر فرم را بررسی کنید.', 'sodino'), 'error');
+ }
- wp_safe_redirect(admin_url('admin.php?page=sodino-banners'));
- exit;
+ $this->redirectWithNotice(admin_url('admin.php?page=sodino-banners'), __('بنر با موفقیت ذخیره شد.', 'sodino'), 'success');
}
public function handleUpsellActions() {
+ if (!current_user_can('manage_options')) {
+ return;
+ }
+
if (!isset($_GET['_wpnonce']) || !in_array($_GET['action'], ['delete_upsell', 'toggle_upsell_status'], true) || !wp_verify_nonce($_GET['_wpnonce'], $_GET['action'])) {
return;
}
@@ -368,6 +490,10 @@ class AdminController {
}
public function handleBannerActions() {
+ if (!current_user_can('manage_options')) {
+ return;
+ }
+
if (!isset($_GET['_wpnonce']) || !in_array($_GET['action'], ['delete_banner', 'toggle_banner_status'], true) || !wp_verify_nonce($_GET['_wpnonce'], $_GET['action'])) {
return;
}
@@ -395,6 +521,10 @@ class AdminController {
}
public function searchProductsAjax() {
+ if (!current_user_can('manage_options')) {
+ wp_send_json([]);
+ }
+
if (!check_ajax_referer('sodino_search_products', 'security', false)) {
wp_send_json([]);
}
@@ -426,13 +556,17 @@ class AdminController {
'plugin_enabled' => 1,
'pricing_enabled' => 1,
'upsell_enabled' => 1,
+ 'banner_enabled' => 1,
'allow_multiple_rules' => 0,
'strategy' => 'priority',
'max_discount_percent' => 100,
'min_product_price' => 0,
+ 'cache_enabled' => 1,
+ 'cache_duration' => 3600,
'ab_testing_enabled' => 0,
'cart_pricing_enabled' => 1,
'scheduled_campaigns_enabled' => 1,
+ 'debug_mode' => 0,
];
}
@@ -449,23 +583,31 @@ class AdminController {
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
}
+ $strategy = sanitize_key($_POST['strategy'] ?? 'priority');
+ if (!in_array($strategy, $this->allowedStrategies, true)) {
+ $strategy = 'priority';
+ }
+
$settings = [
'plugin_enabled' => isset($_POST['plugin_enabled']) ? 1 : 0,
'pricing_enabled' => isset($_POST['pricing_enabled']) ? 1 : 0,
'upsell_enabled' => isset($_POST['upsell_enabled']) ? 1 : 0,
+ 'banner_enabled' => isset($_POST['banner_enabled']) ? 1 : 0,
'allow_multiple_rules' => isset($_POST['allow_multiple_rules']) ? 1 : 0,
- 'strategy' => sanitize_text_field($_POST['strategy'] ?? 'priority'),
+ 'strategy' => $strategy,
'max_discount_percent' => max(0, min(100, floatval($_POST['max_discount_percent'] ?? 100))),
'min_product_price' => max(0, floatval($_POST['min_product_price'] ?? 0)),
+ 'cache_enabled' => isset($_POST['cache_enabled']) ? 1 : 0,
+ 'cache_duration' => max(60, intval($_POST['cache_duration'] ?? 3600)),
'ab_testing_enabled' => isset($_POST['ab_testing_enabled']) ? 1 : 0,
'cart_pricing_enabled' => isset($_POST['cart_pricing_enabled']) ? 1 : 0,
'scheduled_campaigns_enabled' => isset($_POST['scheduled_campaigns_enabled']) ? 1 : 0,
+ 'debug_mode' => isset($_POST['debug_mode']) ? 1 : 0,
];
update_option('sodino_settings', $settings);
- wp_safe_redirect(add_query_arg('updated', 'true', admin_url('admin.php?page=sodino-settings')));
- exit;
+ $this->redirectWithNotice(add_query_arg('updated', 'true', admin_url('admin.php?page=sodino-settings')), __('تنظیمات با موفقیت ذخیره شد.', 'sodino'), 'success');
}
/**
@@ -490,7 +632,11 @@ class AdminController {
* Save rule
*/
private function saveRule($rule = null) {
- if (!isset($_POST['gheymatyar_rule_nonce']) || !wp_verify_nonce($_POST['gheymatyar_rule_nonce'], 'gheymatyar_save_rule')) {
+ if (!current_user_can('manage_options')) {
+ wp_die(__('دسترسی کافی ندارید.', 'sodino'));
+ }
+
+ if (!isset($_POST['sodino_rule_nonce']) || !wp_verify_nonce($_POST['sodino_rule_nonce'], 'sodino_save_rule')) {
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
}
@@ -498,20 +644,53 @@ class AdminController {
$rule = new Rule();
}
- $rule->name = sanitize_text_field($_POST['name'] ?? '');
+ $name = sanitize_text_field($_POST['name'] ?? '');
+ $this->requireValue($name, __('عنوان قانون الزامی است.', 'sodino'), 'sodino-add-rule');
+
+ $conditionType = sanitize_key($_POST['condition_type'] ?? 'user_type');
+ if (!in_array($conditionType, $this->allowedRuleConditionTypes, true)) {
+ $conditionType = 'user_type';
+ }
+
+ $conditionValue = sanitize_text_field($_POST['condition_value'] ?? '');
+ $this->requireValue($conditionValue, __('مقدار شرط قانون الزامی است.', 'sodino'), 'sodino-add-rule');
+
+ $actionType = sanitize_key($_POST['action_type'] ?? 'discount_percent');
+ if (!in_array($actionType, $this->allowedRuleActionTypes, true)) {
+ $actionType = 'discount_percent';
+ }
+
+ $actionValue = sanitize_text_field($_POST['action_value'] ?? '0');
+ if ($actionType !== 'free_shipping' && floatval($actionValue) <= 0) {
+ $this->redirectWithNotice($this->getBackUrl('sodino-add-rule'), __('مقدار عملیات باید بزرگتر از صفر باشد.', 'sodino'), 'error');
+ }
+
+ $rule->name = $name;
$rule->priority = max(1, intval($_POST['priority'] ?? 10));
$rule->usage_limit = max(0, intval($_POST['usage_limit'] ?? 0));
$rule->user_roles = array_map('sanitize_text_field', (array) ($_POST['user_roles'] ?? []));
- $rule->condition_type = sanitize_text_field($_POST['condition_type'] ?? 'user_type');
- $rule->condition_value = sanitize_text_field($_POST['condition_value'] ?? 'new');
- $rule->action_type = sanitize_text_field($_POST['action_type'] ?? 'discount_percent');
- $rule->action_value = sanitize_text_field($_POST['action_value'] ?? '0');
+
+ $rule->conditions = [
+ [
+ 'type' => $conditionType,
+ 'value' => $conditionValue,
+ ],
+ ];
+ $rule->actions = [
+ [
+ 'type' => $actionType,
+ 'value' => $actionValue,
+ ],
+ ];
+ $rule->syncLegacyFields();
$rule->enabled = isset($_POST['enabled']) ? 1 : 0;
- $this->ruleRepository->save($rule);
+ $id = $this->ruleRepository->save($rule);
+ if (!$id) {
+ $this->redirectWithNotice($this->getBackUrl('sodino-add-rule'), __('ذخیره قانون انجام نشد. لطفا مقادیر فرم را بررسی کنید.', 'sodino'), 'error');
+ }
- wp_safe_redirect(admin_url('admin.php?page=sodino-rules'));
- exit;
+ $this->redirectWithNotice(admin_url('admin.php?page=sodino-rules'), __('قانون با موفقیت ذخیره شد.', 'sodino'), 'success');
}
/**
@@ -525,7 +704,7 @@ class AdminController {
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$this->ruleRepository->delete($id);
- wp_redirect(admin_url('admin.php?page=sodino-rules'));
+ wp_safe_redirect(admin_url('admin.php?page=sodino-rules'));
exit;
}
-}
\ No newline at end of file
+}
diff --git a/app/Controllers/RuleController.php b/app/Controllers/RuleController.php
index 01c5103..5c2c09d 100644
--- a/app/Controllers/RuleController.php
+++ b/app/Controllers/RuleController.php
@@ -166,7 +166,6 @@ class RuleController extends BaseController {
$rule->end_date = !empty($_POST['end_date']) ? sanitize_text_field($_POST['end_date']) : null;
$rule->enabled = isset($_POST['enabled']) ? 1 : 0;
- // Parse conditions
if (isset($_POST['conditions']) && is_array($_POST['conditions'])) {
$rule->conditions = array_map(function($condition) {
return [
@@ -174,9 +173,15 @@ class RuleController extends BaseController {
'value' => sanitize_text_field($condition['value'] ?? '')
];
}, $_POST['conditions']);
+ } else {
+ $rule->conditions = [
+ [
+ 'type' => sanitize_text_field($_POST['condition_type'] ?? 'user_type'),
+ 'value' => sanitize_text_field($_POST['condition_value'] ?? 'new'),
+ ],
+ ];
}
- // Parse actions
if (isset($_POST['actions']) && is_array($_POST['actions'])) {
$rule->actions = array_map(function($action) {
return [
@@ -184,6 +189,15 @@ class RuleController extends BaseController {
'value' => sanitize_text_field($action['value'] ?? '')
];
}, $_POST['actions']);
+ } else {
+ $rule->actions = [
+ [
+ 'type' => sanitize_text_field($_POST['action_type'] ?? 'discount_percent'),
+ 'value' => sanitize_text_field($_POST['action_value'] ?? '0'),
+ ],
+ ];
}
+
+ $rule->syncLegacyFields();
}
}
diff --git a/app/Models/Rule.php b/app/Models/Rule.php
index 58f9a90..fcaa0b8 100644
--- a/app/Models/Rule.php
+++ b/app/Models/Rule.php
@@ -18,6 +18,10 @@ class Rule {
public $enabled;
public $created_at;
public $updated_at;
+ public $condition_type;
+ public $condition_value;
+ public $action_type;
+ public $action_value;
/**
* Constructor
@@ -36,6 +40,7 @@ class Rule {
$this->enabled = isset($data['enabled']) ? (int) $data['enabled'] : 1;
$this->created_at = $data['created_at'] ?? null;
$this->updated_at = $data['updated_at'] ?? null;
+ $this->syncLegacyFields();
}
private function parseJsonField($value) {
@@ -59,6 +64,16 @@ class Rule {
return array_filter(array_map('trim', explode(',', $value)));
}
+ public function syncLegacyFields() {
+ $condition = $this->conditions[0] ?? [];
+ $action = $this->actions[0] ?? [];
+
+ $this->condition_type = $condition['type'] ?? 'user_type';
+ $this->condition_value = $condition['value'] ?? 'new';
+ $this->action_type = $action['type'] ?? 'discount_percent';
+ $this->action_value = $action['value'] ?? 0;
+ }
+
/**
* Convert to array
*/
diff --git a/app/Repositories/BannerRepository.php b/app/Repositories/BannerRepository.php
index 07e231d..191cb72 100644
--- a/app/Repositories/BannerRepository.php
+++ b/app/Repositories/BannerRepository.php
@@ -46,12 +46,20 @@ class BannerRepository {
unset($data['id'], $data['created_at']);
if ($banner->id) {
- $wpdb->update($this->table_name, $data, ['id' => $banner->id]);
+ $result = $wpdb->update($this->table_name, $data, ['id' => $banner->id]);
+ if ($result === false) {
+ return false;
+ }
+
$this->clearCache();
return $banner->id;
}
- $wpdb->insert($this->table_name, $data);
+ $result = $wpdb->insert($this->table_name, $data);
+ if ($result === false) {
+ return false;
+ }
+
$this->clearCache();
return $wpdb->insert_id;
}
@@ -76,6 +84,12 @@ class BannerRepository {
}
public function clearCache() {
- wp_cache_flush();
+ $version = microtime(true);
+ update_option('sodino_banners_cache_version', $version, false);
+ wp_cache_set('version', $version, 'sodino_banners');
+
+ if (function_exists('wp_cache_flush_group')) {
+ wp_cache_flush_group('sodino_banners');
+ }
}
}
diff --git a/app/Repositories/RuleRepository.php b/app/Repositories/RuleRepository.php
index 6a76af3..c655357 100644
--- a/app/Repositories/RuleRepository.php
+++ b/app/Repositories/RuleRepository.php
@@ -87,10 +87,18 @@ class RuleRepository {
unset($data['id'], $data['created_at'], $data['updated_at']);
if ($rule->id) {
- $wpdb->update($this->table_name, $data, ['id' => $rule->id]);
+ $result = $wpdb->update($this->table_name, $data, ['id' => $rule->id]);
+ if ($result === false) {
+ return false;
+ }
+
$id = $rule->id;
} else {
- $wpdb->insert($this->table_name, $data);
+ $result = $wpdb->insert($this->table_name, $data);
+ if ($result === false) {
+ return false;
+ }
+
$id = $wpdb->insert_id;
}
diff --git a/app/Repositories/UpsellRepository.php b/app/Repositories/UpsellRepository.php
index 273ed9a..7260ec1 100644
--- a/app/Repositories/UpsellRepository.php
+++ b/app/Repositories/UpsellRepository.php
@@ -46,11 +46,19 @@ class UpsellRepository {
unset($data['id'], $data['created_at'], $data['updated_at']);
if ($upsell->id) {
- $wpdb->update($this->table_name, $data, ['id' => $upsell->id]);
+ $result = $wpdb->update($this->table_name, $data, ['id' => $upsell->id]);
+ if ($result === false) {
+ return false;
+ }
+
return $upsell->id;
}
- $wpdb->insert($this->table_name, $data);
+ $result = $wpdb->insert($this->table_name, $data);
+ if ($result === false) {
+ return false;
+ }
+
return $wpdb->insert_id;
}
diff --git a/app/Services/BannerService.php b/app/Services/BannerService.php
index 3f915a8..05763b2 100644
--- a/app/Services/BannerService.php
+++ b/app/Services/BannerService.php
@@ -106,6 +106,12 @@ class BannerService {
}
private function getCacheKey($position, array $context) {
- return 'sodino_active_banners_' . md5($position . '|' . serialize($context));
+ $version = wp_cache_get('version', 'sodino_banners');
+ if ($version === false) {
+ $version = get_option('sodino_banners_cache_version', 1);
+ wp_cache_set('version', $version, 'sodino_banners');
+ }
+
+ return 'sodino_active_banners_' . md5($version . '|' . $position . '|' . serialize($context));
}
}
diff --git a/app/Services/PricingService.php b/app/Services/PricingService.php
index 68a0096..d49f0e3 100644
--- a/app/Services/PricingService.php
+++ b/app/Services/PricingService.php
@@ -11,6 +11,7 @@ class PricingService {
private $trackingService;
private $settings;
private $cache;
+ private $trackedApplications = [];
public function __construct(RuleRepository $ruleRepository, TrackingService $trackingService) {
$this->ruleRepository = $ruleRepository;
@@ -51,8 +52,7 @@ class PricingService {
$price = $this->applyRuleActions($rule, $price);
if ($price < $oldPrice) {
- $this->trackingService->recordDiscountApplied($product, $oldPrice, $price, $rule->id);
- $this->ruleRepository->incrementUsage($rule->id);
+ $this->trackDiscountOnce($product, $oldPrice, $price, $rule->id);
}
}
@@ -206,7 +206,7 @@ class PricingService {
return false;
}
- return (bool) array_intersect($product_cats, array_map('intval', $categories));
+ return (bool) array_intersect($product_cats, $this->normalizeIdList($categories));
}
private function productIsInIds($product, $ids) {
@@ -214,7 +214,23 @@ class PricingService {
return false;
}
- return in_array($product->get_id(), array_map('intval', $ids), true);
+ return in_array($product->get_id(), $this->normalizeIdList($ids), true);
+ }
+
+ private function normalizeIdList($value) {
+ $values = is_array($value) ? $value : [$value];
+ $ids = [];
+
+ foreach ($values as $item) {
+ foreach (explode(',', (string) $item) as $id) {
+ $id = absint(trim($id));
+ if ($id > 0) {
+ $ids[] = $id;
+ }
+ }
+ }
+
+ return array_values(array_unique($ids));
}
private function applyRuleActions($rule, $price) {
@@ -273,4 +289,17 @@ class PricingService {
}
return 0;
}
+
+ private function trackDiscountOnce($product, $oldPrice, $price, $ruleId) {
+ $productId = $product ? $product->get_id() : 0;
+ $key = implode(':', [$productId, (int) $ruleId, round($oldPrice, 4), round($price, 4)]);
+
+ if (isset($this->trackedApplications[$key])) {
+ return;
+ }
+
+ $this->trackedApplications[$key] = true;
+ $this->trackingService->recordDiscountApplied($product, $oldPrice, $price, $ruleId);
+ $this->ruleRepository->incrementUsage($ruleId);
+ }
}
diff --git a/public/hooks/banner-hooks.php b/public/hooks/banner-hooks.php
index f7d7f61..3ae6509 100644
--- a/public/hooks/banner-hooks.php
+++ b/public/hooks/banner-hooks.php
@@ -11,7 +11,8 @@ global $sodino_banner_service;
$bannerRepository = new BannerRepository();
$sodino_banner_service = new BannerService($bannerRepository);
-add_action('wp_head', 'sodino_render_top_banner', 1);
+add_action('wp_body_open', 'sodino_render_top_banner', 5);
+add_action('wp_footer', 'sodino_render_top_banner_fallback', 5);
add_filter('the_content', 'sodino_render_middle_banner');
add_action('wp_footer', 'sodino_render_bottom_banner', 20);
add_action('woocommerce_after_single_product_summary', 'sodino_render_product_banner', 5);
@@ -101,13 +102,28 @@ function sodino_render_banner_position($position) {
}
function sodino_render_top_banner() {
+ static $rendered = false;
+
if (is_admin()) {
return;
}
+ if ($rendered) {
+ return;
+ }
+
+ $rendered = true;
echo sodino_render_banner_position('top');
}
+function sodino_render_top_banner_fallback() {
+ if (did_action('wp_body_open')) {
+ return;
+ }
+
+ sodino_render_top_banner();
+}
+
function sodino_render_middle_banner($content) {
if (is_admin() || !is_singular() || !in_the_loop() || is_feed()) {
return $content;
@@ -146,7 +162,8 @@ function sodino_render_cart_banner() {
}
function sodino_handle_banner_click() {
- if (!isset($_POST['banner_id']) || !isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'sodino_banner_click')) {
+ $nonce = $_POST['nonce'] ?? ($_POST['security'] ?? '');
+ if (!isset($_POST['banner_id']) || !wp_verify_nonce($nonce, 'sodino_banner_click')) {
wp_send_json_error();
}
diff --git a/public/hooks/pricing-hooks.php b/public/hooks/pricing-hooks.php
index 99bf896..a3bdb67 100644
--- a/public/hooks/pricing-hooks.php
+++ b/public/hooks/pricing-hooks.php
@@ -22,17 +22,7 @@ add_filter('woocommerce_product_get_sale_price', 'sodino_apply_dynamic_pricing',
add_filter('woocommerce_product_variation_get_price', 'sodino_apply_dynamic_pricing', 10, 2);
add_filter('woocommerce_product_variation_get_sale_price', 'sodino_apply_dynamic_pricing', 10, 2);
-// Also hook into cart and checkout prices
-add_filter('woocommerce_cart_item_price', 'sodino_apply_to_cart_item', 10, 3);
-add_filter('woocommerce_cart_item_subtotal', 'sodino_apply_to_cart_item', 10, 3);
-
function sodino_apply_dynamic_pricing($price, $product) {
global $sodino_pricing_service;
return $sodino_pricing_service->applyDynamicPricing($price, $product);
}
-
-function sodino_apply_to_cart_item($price, $cart_item, $cart_item_key) {
- global $sodino_pricing_service;
- $product = $cart_item['data'];
- return wc_price($sodino_pricing_service->applyDynamicPricing($product->get_price(), $product));
-}
\ No newline at end of file
diff --git a/public/js/banner-frontend.js b/public/js/banner-frontend.js
index 5f87fa1..3b56c89 100644
--- a/public/js/banner-frontend.js
+++ b/public/js/banner-frontend.js
@@ -22,7 +22,7 @@
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
- body: 'action=sodino_banner_click&banner_id=' + encodeURIComponent(bannerId) + '&security=' + encodeURIComponent(window.sodinoBannerFrontend.nonce),
+ body: 'action=sodino_banner_click&banner_id=' + encodeURIComponent(bannerId) + '&nonce=' + encodeURIComponent(window.sodinoBannerFrontend.nonce),
});
}