Add .gitignore and .htaccess for security

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
info@myprestarocks
2025-12-31 19:40:28 +01:00
commit 0feba3038f
8 changed files with 1338 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
# IDE
.idea/
.vscode/
*.sublime-*
# OS
.DS_Store
Thumbs.db
# Node (if any build tools)
node_modules/
# Composer dev dependencies (if installed locally)
vendor/

10
.htaccess Normal file
View File

@@ -0,0 +1,10 @@
# Apache 2.2
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>
# Apache 2.4
<IfModule mod_authz_core.c>
Require all denied
</IfModule>

27
composer.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "myprestarocks/prestashop-session",
"description": "Shared session tracking for PrestaShop modules - bot detection, browser/device/OS detection, session hash generation, shared mpr_sessions table",
"keywords": ["prestashop", "session", "tracking", "bot-detection", "analytics"],
"type": "library",
"license": "MIT",
"authors": [
{
"name": "mypresta.rocks",
"email": "info@mypresta.rocks"
}
],
"require": {
"php": ">=7.1"
},
"autoload": {
"psr-4": {
"MyPrestaRocks\\Session\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-main": "1.0.x-dev"
}
},
"minimum-stability": "stable"
}

262
src/CartActionsTable.php Normal file
View File

@@ -0,0 +1,262 @@
<?php
/**
* Cart Actions Table - Cart action tracking
*
* Handles installation and management of the mpr_cart_actions table.
* Tracks add/remove/update/voucher actions for cart analysis.
*
* Usage:
* use MyPrestaRocks\Session\CartActionsTable;
*
* // In module install():
* CartActionsTable::install();
*
* // In module uninstall():
* CartActionsTable::uninstall('yourmodulename');
*
* @author mypresta.rocks <info@mypresta.rocks>
* @copyright Copyright (c) mypresta.rocks
* @license MIT
*/
namespace MyPrestaRocks\Session;
if (!defined('_PS_VERSION_')) {
exit;
}
class CartActionsTable
{
/**
* Table name without prefix
*/
private const TABLE_NAME = 'mpr_cart_actions';
/**
* Get full table name with prefix
*
* @return string
*/
public static function getTableName()
{
return _DB_PREFIX_ . self::TABLE_NAME;
}
/**
* Check if the table exists
*
* @return bool
*/
public static function tableExists()
{
$sql = "SHOW TABLES LIKE '" . self::getTableName() . "'";
return (bool) \Db::getInstance()->getValue($sql);
}
/**
* Install the cart actions table
*
* @return bool
*/
public static function install()
{
$engine = _MYSQL_ENGINE_;
$charset = 'utf8mb4';
$table = self::getTableName();
$sql = "CREATE TABLE IF NOT EXISTS `{$table}` (
`id_cart_action` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`id_session` INT(10) UNSIGNED NOT NULL,
`id_cart` INT(10) UNSIGNED NOT NULL,
`id_product` INT(10) UNSIGNED NOT NULL,
`id_product_attribute` INT(10) UNSIGNED DEFAULT 0,
`action_type` ENUM('add', 'remove', 'update', 'voucher_add', 'voucher_remove') NOT NULL,
`quantity` INT(10) UNSIGNED NOT NULL DEFAULT 1,
`quantity_before` INT(10) UNSIGNED DEFAULT NULL,
`product_name` VARCHAR(255) DEFAULT NULL,
`product_price` DECIMAL(20,6) DEFAULT NULL,
`date_add` DATETIME NOT NULL,
PRIMARY KEY (`id_cart_action`),
KEY `idx_session` (`id_session`),
KEY `idx_cart` (`id_cart`),
KEY `idx_product` (`id_product`),
KEY `idx_action_type` (`action_type`),
KEY `idx_date_add` (`date_add`),
KEY `idx_session_date` (`id_session`, `date_add`)
) ENGINE={$engine} DEFAULT CHARSET={$charset};";
return \Db::getInstance()->execute($sql);
}
/**
* Uninstall the cart actions table
*
* @param string $currentModule The module being uninstalled
* @return bool
*/
public static function uninstall($currentModule)
{
foreach (SessionTable::getModulesUsingSession() as $moduleName) {
if ($moduleName === $currentModule) {
continue;
}
if (\Module::isInstalled($moduleName)) {
return true;
}
}
return \Db::getInstance()->execute("DROP TABLE IF EXISTS `" . self::getTableName() . "`");
}
/**
* Track a cart action
*
* @param int $idSession Session ID
* @param int $idCart Cart ID
* @param int $idProduct Product ID
* @param int $idProductAttribute Product attribute ID (0 if none)
* @param string $actionType 'add', 'remove', 'update', 'voucher_add', or 'voucher_remove'
* @param int $quantity Current/new quantity
* @param int|null $quantityBefore Previous quantity (for update actions)
* @param string|null $productName Product name or voucher name
* @param float|null $productPrice Product price or voucher value
* @return bool
*/
public static function trackCartAction(
$idSession,
$idCart,
$idProduct,
$idProductAttribute,
$actionType,
$quantity,
$quantityBefore = null,
$productName = null,
$productPrice = null
) {
if (!$idSession || !$idCart) {
return false;
}
$validActions = ['add', 'remove', 'update', 'voucher_add', 'voucher_remove'];
if (!in_array($actionType, $validActions)) {
return false;
}
$isVoucherAction = strpos($actionType, 'voucher_') === 0;
if (!$isVoucherAction && !$idProduct) {
return false;
}
// Look up product name if not provided (only for product actions)
if ($productName === null && !$isVoucherAction && $idProduct) {
$idLang = (int) \Context::getContext()->language->id;
$productName = \Db::getInstance()->getValue(
'SELECT name FROM ' . _DB_PREFIX_ . 'product_lang
WHERE id_product = ' . (int) $idProduct . '
AND id_lang = ' . $idLang
);
}
// Look up price if not provided
if ($productPrice === null && $idProduct) {
$productPrice = \Product::getPriceStatic(
(int) $idProduct,
true,
(int) $idProductAttribute ?: null,
6
);
}
$table = self::getTableName();
$sql = "INSERT INTO `{$table}`
(id_session, id_cart, id_product, id_product_attribute, action_type,
quantity, quantity_before, product_name, product_price, date_add)
VALUES (
" . (int) $idSession . ",
" . (int) $idCart . ",
" . (int) $idProduct . ",
" . (int) $idProductAttribute . ",
'" . pSQL($actionType) . "',
" . (int) $quantity . ",
" . ($quantityBefore !== null ? (int) $quantityBefore : 'NULL') . ",
" . ($productName ? "'" . pSQL(substr($productName, 0, 255)) . "'" : 'NULL') . ",
" . ($productPrice !== null ? (float) $productPrice : 'NULL') . ",
NOW()
)";
return \Db::getInstance()->execute($sql);
}
/**
* Get cart actions for a session
*
* @param int $idSession Session ID
* @param int $limit Max records
* @return array
*/
public static function getCartActionsForSession($idSession, $limit = 100)
{
$table = self::getTableName();
return \Db::getInstance()->executeS(
"SELECT * FROM `{$table}`
WHERE id_session = " . (int) $idSession . "
ORDER BY date_add ASC
LIMIT " . (int) $limit
) ?: [];
}
/**
* Get cart actions for multiple sessions (batch lookup)
*
* @param array $sessionIds Array of session IDs
* @param int $limit Max records per session
* @return array Grouped by session ID
*/
public static function getCartActionsForSessions(array $sessionIds, $limit = 100)
{
if (empty($sessionIds)) {
return [];
}
$table = self::getTableName();
$ids = array_map('intval', $sessionIds);
$actions = \Db::getInstance()->executeS(
"SELECT * FROM `{$table}`
WHERE id_session IN (" . implode(',', $ids) . ")
ORDER BY id_session, date_add ASC"
) ?: [];
$grouped = [];
foreach ($actions as $action) {
$sessId = $action['id_session'];
if (!isset($grouped[$sessId])) {
$grouped[$sessId] = [];
}
if (count($grouped[$sessId]) < $limit) {
$grouped[$sessId][] = $action;
}
}
return $grouped;
}
/**
* Clean old cart action records
*
* @param int $hours Hours to keep
* @return bool
*/
public static function cleanOldCartActions($hours = 168)
{
$table = self::getTableName();
return \Db::getInstance()->execute(
"DELETE FROM `{$table}`
WHERE date_add < DATE_SUB(NOW(), INTERVAL " . (int) $hours . " HOUR)"
);
}
}

189
src/PageViewsTable.php Normal file
View File

@@ -0,0 +1,189 @@
<?php
/**
* Page Views Table - Detailed page view tracking
*
* Handles installation and management of the mpr_page_views table.
* This is an optional table for detailed funnel analysis.
*
* Usage:
* use MyPrestaRocks\Session\PageViewsTable;
*
* // In module install():
* PageViewsTable::install();
*
* // In module uninstall():
* PageViewsTable::uninstall('yourmodulename');
*
* @author mypresta.rocks <info@mypresta.rocks>
* @copyright Copyright (c) mypresta.rocks
* @license MIT
*/
namespace MyPrestaRocks\Session;
if (!defined('_PS_VERSION_')) {
exit;
}
class PageViewsTable
{
/**
* Table name without prefix
*/
private const TABLE_NAME = 'mpr_page_views';
/**
* Get full table name with prefix
*
* @return string
*/
public static function getTableName()
{
return _DB_PREFIX_ . self::TABLE_NAME;
}
/**
* Check if the table exists
*
* @return bool
*/
public static function tableExists()
{
$sql = "SHOW TABLES LIKE '" . self::getTableName() . "'";
return (bool) \Db::getInstance()->getValue($sql);
}
/**
* Install the page views table
*
* @return bool
*/
public static function install()
{
$engine = _MYSQL_ENGINE_;
$charset = 'utf8mb4';
$table = self::getTableName();
$sql = "CREATE TABLE IF NOT EXISTS `{$table}` (
`id_page_view` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`id_session` INT(10) UNSIGNED NOT NULL,
`page_type` TINYINT(2) UNSIGNED NOT NULL,
`page_id` INT(10) UNSIGNED DEFAULT NULL,
`controller` VARCHAR(64) DEFAULT NULL,
`date_add` DATETIME NOT NULL,
PRIMARY KEY (`id_page_view`),
KEY `id_session` (`id_session`),
KEY `page_type` (`page_type`),
KEY `date_add` (`date_add`),
KEY `session_date` (`id_session`, `date_add`)
) ENGINE={$engine} DEFAULT CHARSET={$charset};";
return \Db::getInstance()->execute($sql);
}
/**
* Uninstall the page views table
*
* @param string $currentModule The module being uninstalled
* @return bool
*/
public static function uninstall($currentModule)
{
foreach (SessionTable::getModulesUsingSession() as $moduleName) {
if ($moduleName === $currentModule) {
continue;
}
if (\Module::isInstalled($moduleName)) {
return true;
}
}
return \Db::getInstance()->execute("DROP TABLE IF EXISTS `" . self::getTableName() . "`");
}
/**
* Track detailed page view
*
* @param int $idSession Session ID
* @param int $pageType Page type constant
* @param int|null $pageId Page ID
* @param string|null $controller Controller name
* @return bool
*/
public static function trackPageView($idSession, $pageType, $pageId = null, $controller = null)
{
if (!$idSession) {
return false;
}
$table = self::getTableName();
$sql = "INSERT INTO `{$table}` (id_session, page_type, page_id, controller, date_add)
VALUES (" . (int) $idSession . ", " . (int) $pageType . ", " .
($pageId !== null ? (int) $pageId : 'NULL') . ", " .
($controller ? "'" . pSQL(substr($controller, 0, 64)) . "'" : 'NULL') . ", NOW())";
return \Db::getInstance()->execute($sql);
}
/**
* Get page views for a session
*
* @param int $idSession Session ID
* @param int $limit Max records to return
* @return array
*/
public static function getPageViewsForSession($idSession, $limit = 100)
{
$table = self::getTableName();
$sql = "SELECT * FROM `{$table}`
WHERE id_session = " . (int) $idSession . "
ORDER BY date_add ASC
LIMIT " . (int) $limit;
return \Db::getInstance()->executeS($sql) ?: [];
}
/**
* Get page view statistics for a session
*
* @param int $idSession Session ID
* @return array
*/
public static function getPageViewStats($idSession)
{
$table = self::getTableName();
$sql = "SELECT page_type, COUNT(*) as count
FROM `{$table}`
WHERE id_session = " . (int) $idSession . "
GROUP BY page_type";
$byType = \Db::getInstance()->executeS($sql) ?: [];
$stats = ['by_type' => []];
foreach ($byType as $row) {
$stats['by_type'][$row['page_type']] = (int) $row['count'];
}
return $stats;
}
/**
* Clean old page view records
*
* @param int $hours Hours to keep
* @return bool
*/
public static function cleanOldPageViews($hours = 24)
{
$table = self::getTableName();
return \Db::getInstance()->execute(
"DELETE FROM `{$table}`
WHERE date_add < DATE_SUB(NOW(), INTERVAL " . (int) $hours . " HOUR)"
);
}
}

77
src/SessionInstaller.php Normal file
View File

@@ -0,0 +1,77 @@
<?php
/**
* Session Installer - Convenience class for installing all session tables
*
* Provides a single entry point for installing and uninstalling
* all shared session tables (mpr_sessions, mpr_page_views, mpr_cart_actions).
*
* Usage:
* use MyPrestaRocks\Session\SessionInstaller;
*
* // In module install():
* SessionInstaller::installAll();
*
* // In module uninstall():
* SessionInstaller::uninstallAll('yourmodulename');
*
* @author mypresta.rocks <info@mypresta.rocks>
* @copyright Copyright (c) mypresta.rocks
* @license MIT
*/
namespace MyPrestaRocks\Session;
if (!defined('_PS_VERSION_')) {
exit;
}
class SessionInstaller
{
/**
* Install all shared session tables
*
* @return bool
*/
public static function installAll()
{
$result = true;
$result = $result && SessionTable::install();
$result = $result && PageViewsTable::install();
$result = $result && CartActionsTable::install();
return $result;
}
/**
* Uninstall all shared session tables
* Only drops tables if no other module using them is installed
*
* @param string $currentModule The module being uninstalled
* @return bool
*/
public static function uninstallAll($currentModule)
{
$result = true;
$result = $result && CartActionsTable::uninstall($currentModule);
$result = $result && PageViewsTable::uninstall($currentModule);
$result = $result && SessionTable::uninstall($currentModule);
return $result;
}
/**
* Clean up old data from all session tables
*
* @param int $sessionHours Hours to keep sessions (default 24)
* @param int $pageViewsHours Hours to keep page views (default 24)
* @param int $cartActionsHours Hours to keep cart actions (default 168 / 7 days)
* @return bool
*/
public static function cleanAll($sessionHours = 24, $pageViewsHours = 24, $cartActionsHours = 168)
{
$result = true;
$result = $result && SessionTable::cleanOldSessions($sessionHours);
$result = $result && PageViewsTable::cleanOldPageViews($pageViewsHours);
$result = $result && CartActionsTable::cleanOldCartActions($cartActionsHours);
return $result;
}
}

395
src/SessionTable.php Normal file
View File

@@ -0,0 +1,395 @@
<?php
/**
* Session Table - Shared mpr_sessions table management
*
* Handles installation and uninstallation of the shared mpr_sessions table.
* Multiple modules can use this table, and it's only dropped when the last
* module using it is uninstalled.
*
* Usage:
* use MyPrestaRocks\Session\SessionTable;
*
* // In module install():
* SessionTable::install();
*
* // In module uninstall():
* SessionTable::uninstall('yourmodulename');
*
* @author mypresta.rocks <info@mypresta.rocks>
* @copyright Copyright (c) mypresta.rocks
* @license MIT
*/
namespace MyPrestaRocks\Session;
if (!defined('_PS_VERSION_')) {
exit;
}
class SessionTable
{
/**
* List of modules that use the shared mpr_sessions table.
* Add your module name here when integrating.
*/
private const MODULES_USING_SESSION = [
'mprexpresscheckout',
'mprtotaldefender',
'mprtradeaccount',
];
/**
* Table name without prefix
*/
private const TABLE_NAME = 'mpr_sessions';
/**
* Get full table name with prefix
*
* @return string
*/
public static function getTableName()
{
return _DB_PREFIX_ . self::TABLE_NAME;
}
/**
* Check if the session table exists
*
* @return bool
*/
public static function tableExists()
{
$sql = "SHOW TABLES LIKE '" . self::getTableName() . "'";
return (bool) \Db::getInstance()->getValue($sql);
}
/**
* Install the shared session table
* Uses IF NOT EXISTS so any module can safely call this
*
* @return bool
*/
public static function install()
{
$engine = _MYSQL_ENGINE_;
$charset = 'utf8mb4';
$table = self::getTableName();
$sql = "CREATE TABLE IF NOT EXISTS `{$table}` (
`id_session` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`id_customer` INT(10) UNSIGNED DEFAULT NULL,
`id_guest` INT(10) UNSIGNED NOT NULL,
`session_hash` VARCHAR(32) NOT NULL,
-- Core tracking (all modules)
`ip_address` VARCHAR(45) DEFAULT NULL,
`user_agent` VARCHAR(512) DEFAULT NULL,
`browser` VARCHAR(32) DEFAULT NULL,
`device_type` VARCHAR(16) DEFAULT NULL,
`os` VARCHAR(32) DEFAULT NULL,
`is_bot` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
-- Attribution tracking (mprexpresscheckout)
`source_type` TINYINT(3) UNSIGNED NOT NULL DEFAULT 0,
`source_detail` VARCHAR(500) DEFAULT NULL,
`utm_source` VARCHAR(128) DEFAULT NULL,
`utm_medium` VARCHAR(128) DEFAULT NULL,
`utm_campaign` VARCHAR(128) DEFAULT NULL,
`utm_term` VARCHAR(255) DEFAULT NULL,
`utm_content` VARCHAR(255) DEFAULT NULL,
`gclid` VARCHAR(255) DEFAULT NULL,
`fbclid` VARCHAR(255) DEFAULT NULL,
`msclkid` VARCHAR(255) DEFAULT NULL,
`ttclid` VARCHAR(255) DEFAULT NULL,
-- Landing page tracking (mprexpresscheckout)
`landing_page_type` TINYINT(2) UNSIGNED DEFAULT NULL,
`landing_page_id` INT(10) UNSIGNED DEFAULT NULL,
`landing_url` VARCHAR(500) DEFAULT NULL,
-- Context
`id_lang` INT(10) UNSIGNED DEFAULT NULL,
`id_currency` INT(10) UNSIGNED DEFAULT NULL,
`is_first_visit` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
-- Page tracking (lightweight counters)
`pages_viewed` INT(10) UNSIGNED NOT NULL DEFAULT 0,
`last_page_type` TINYINT(2) UNSIGNED DEFAULT NULL,
`last_page_id` INT(10) UNSIGNED DEFAULT NULL,
-- Timestamps
`date_add` DATETIME NOT NULL,
`date_last_activity` DATETIME NOT NULL,
PRIMARY KEY (`id_session`),
UNIQUE KEY `session_hash` (`session_hash`),
KEY `id_customer` (`id_customer`),
KEY `id_guest` (`id_guest`),
KEY `ip_address` (`ip_address`),
KEY `source_type` (`source_type`),
KEY `is_bot` (`is_bot`),
KEY `date_add` (`date_add`),
KEY `date_last_activity` (`date_last_activity`),
KEY `customer_sessions` (`id_customer`, `date_add`),
KEY `guest_sessions` (`id_guest`, `date_add`)
) ENGINE={$engine} DEFAULT CHARSET={$charset};";
return \Db::getInstance()->execute($sql);
}
/**
* Uninstall the shared session table
* Only drops the table if no other module using it is still installed
*
* @param string $currentModule The module being uninstalled
* @return bool
*/
public static function uninstall($currentModule)
{
// Check if any other module using sessions is still installed
foreach (self::MODULES_USING_SESSION as $moduleName) {
if ($moduleName === $currentModule) {
continue;
}
if (\Module::isInstalled($moduleName)) {
// Another module still needs the table
return true;
}
}
// No other module needs it - drop the table
return \Db::getInstance()->execute("DROP TABLE IF EXISTS `" . self::getTableName() . "`");
}
/**
* Get list of modules that use the session table
*
* @return array
*/
public static function getModulesUsingSession()
{
return self::MODULES_USING_SESSION;
}
/**
* Check if another module using the session table is installed
*
* @param string $excludeModule Module to exclude from check
* @return bool
*/
public static function isAnotherModuleInstalled($excludeModule)
{
foreach (self::MODULES_USING_SESSION as $moduleName) {
if ($moduleName === $excludeModule) {
continue;
}
if (\Module::isInstalled($moduleName)) {
return true;
}
}
return false;
}
/**
* Update session last activity (with 1-minute throttling)
*
* @param int $idSession Session ID
* @return bool
*/
public static function updateSessionActivity($idSession)
{
$table = self::getTableName();
// Check if recently updated (within 1 minute)
$sql = "SELECT 1 FROM `{$table}`
WHERE id_session = " . (int) $idSession . "
AND date_last_activity > DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
if (\Db::getInstance()->getValue($sql)) {
return true; // Skip update - already recent
}
return \Db::getInstance()->execute(
"UPDATE `{$table}`
SET date_last_activity = NOW()
WHERE id_session = " . (int) $idSession
);
}
/**
* Get active session by hash
*
* @param string $sessionHash
* @param int $timeoutMinutes
* @return int|null Session ID or null
*/
public static function getActiveSessionId($sessionHash, $timeoutMinutes = 60)
{
$table = self::getTableName();
$sql = "SELECT id_session FROM `{$table}`
WHERE session_hash = '" . pSQL($sessionHash) . "'
AND date_last_activity > DATE_SUB(NOW(), INTERVAL " . (int) $timeoutMinutes . " MINUTE)";
$result = \Db::getInstance()->getValue($sql);
return $result ? (int) $result : null;
}
/**
* Link guest sessions to customer (on login/registration)
*
* @param int $idGuest
* @param int $idCustomer
* @return bool
*/
public static function linkGuestToCustomer($idGuest, $idCustomer)
{
if (!$idGuest || !$idCustomer) {
return false;
}
return \Db::getInstance()->update(
self::TABLE_NAME,
['id_customer' => (int) $idCustomer],
'id_guest = ' . (int) $idGuest . ' AND id_customer IS NULL'
);
}
/**
* Clean old sessions
*
* @param int $hours Hours to keep (default 24)
* @return bool
*/
public static function cleanOldSessions($hours = 24)
{
$table = self::getTableName();
return \Db::getInstance()->execute(
"DELETE FROM `{$table}`
WHERE date_last_activity < DATE_SUB(NOW(), INTERVAL " . (int) $hours . " HOUR)"
);
}
/**
* Get active session count
*
* @param int $timeoutMinutes
* @param bool $excludeBots
* @return int
*/
public static function getActiveSessionCount($timeoutMinutes = 60, $excludeBots = true)
{
$table = self::getTableName();
$sql = "SELECT COUNT(*) FROM `{$table}`
WHERE date_last_activity > DATE_SUB(NOW(), INTERVAL " . (int) $timeoutMinutes . " MINUTE)";
if ($excludeBots) {
$sql .= " AND is_bot = 0";
}
return (int) \Db::getInstance()->getValue($sql);
}
/**
* Get sessions for an IP address
*
* @param string $ipAddress
* @param int $hours Time window (0 for all)
* @return array
*/
public static function getSessionsForIP($ipAddress, $hours = 24)
{
$table = self::getTableName();
$sql = "SELECT * FROM `{$table}` WHERE ip_address = '" . pSQL($ipAddress) . "'";
if ($hours > 0) {
$sql .= " AND date_add >= DATE_SUB(NOW(), INTERVAL " . (int) $hours . " HOUR)";
}
$sql .= " ORDER BY date_add DESC";
return \Db::getInstance()->executeS($sql);
}
/**
* Count sessions for an IP address
*
* @param string $ipAddress
* @param int $hours Time window
* @param bool $excludeBots Exclude bot sessions
* @return int
*/
public static function countSessionsForIP($ipAddress, $hours = 24, $excludeBots = true)
{
$table = self::getTableName();
$sql = "SELECT COUNT(*) FROM `{$table}` WHERE ip_address = '" . pSQL($ipAddress) . "'";
if ($excludeBots) {
$sql .= " AND is_bot = 0";
}
if ($hours > 0) {
$sql .= " AND date_add >= DATE_SUB(NOW(), INTERVAL " . (int) $hours . " HOUR)";
}
return (int) \Db::getInstance()->getValue($sql);
}
/**
* Track page view for a session (lightweight - just increment counter)
*
* @param int $idSession Session ID
* @param int|null $pageType Page type constant
* @param int|null $pageId Page ID (product, category, etc.)
* @return bool
*/
public static function trackPageView($idSession, $pageType = null, $pageId = null)
{
if (!$idSession) {
return false;
}
$table = self::getTableName();
$sql = "UPDATE `{$table}` SET
pages_viewed = pages_viewed + 1,
last_page_type = " . ($pageType !== null ? (int) $pageType : 'NULL') . ",
last_page_id = " . ($pageId !== null ? (int) $pageId : 'NULL') . ",
date_last_activity = NOW()
WHERE id_session = " . (int) $idSession;
return \Db::getInstance()->execute($sql);
}
/**
* Track page view by session hash (optimized - single query when session exists)
*
* @param string $sessionHash Session hash
* @param int|null $pageType Page type constant
* @param int|null $pageId Page ID
* @param int $timeoutMinutes Session timeout
* @return bool True if page was tracked, false if session not found
*/
public static function trackPageViewByHash($sessionHash, $pageType = null, $pageId = null, $timeoutMinutes = 60)
{
$table = self::getTableName();
$sql = "UPDATE `{$table}` SET
pages_viewed = pages_viewed + 1,
last_page_type = " . ($pageType !== null ? (int) $pageType : 'NULL') . ",
last_page_id = " . ($pageId !== null ? (int) $pageId : 'NULL') . ",
date_last_activity = NOW()
WHERE session_hash = '" . pSQL($sessionHash) . "'
AND date_last_activity > DATE_SUB(NOW(), INTERVAL " . (int) $timeoutMinutes . " MINUTE)";
\Db::getInstance()->execute($sql);
return \Db::getInstance()->Affected_Rows() > 0;
}
}

364
src/SessionTrait.php Normal file
View File

@@ -0,0 +1,364 @@
<?php
/**
* Session Trait - Shared session detection and tracking for PrestaShop modules
*
* Provides common session functionality used by multiple MPR modules:
* - Bot detection
* - Browser detection
* - Device type detection
* - OS detection
* - Session hash generation
* - IP address handling
* - User agent handling
*
* Usage:
* use MyPrestaRocks\Session\SessionTrait;
*
* class YourSessionClass extends ObjectModel {
* use SessionTrait;
* }
*
* @author mypresta.rocks <info@mypresta.rocks>
* @copyright Copyright (c) mypresta.rocks
* @license MIT
*/
namespace MyPrestaRocks\Session;
if (!defined('_PS_VERSION_')) {
exit;
}
trait SessionTrait
{
/**
* Page type values for last_page_type column
* Note: Using static array instead of constants for PHP 7.4 compatibility
* (traits cannot have constants in PHP < 8.2)
*/
protected static $PAGE_TYPES = [
'HOME' => 1,
'CATEGORY' => 2,
'PRODUCT' => 3,
'CMS' => 4,
'CART' => 5,
'CHECKOUT' => 6,
'ORDER_CONFIRMATION' => 7,
'MY_ACCOUNT' => 8,
'SEARCH' => 9,
'MANUFACTURER' => 10,
'SUPPLIER' => 11,
'CONTACT' => 12,
'OTHER' => 99,
];
/**
* Detect if request is from a bot
*
* @param string|null $userAgent Optional user agent (uses $_SERVER if not provided)
* @return bool
*/
protected static function mprIsBot($userAgent = null)
{
if ($userAgent === null) {
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
return true;
}
$userAgent = $_SERVER['HTTP_USER_AGENT'];
}
$userAgent = strtolower($userAgent);
$botPatterns = [
// Search engine crawlers
'googlebot', 'bingbot', 'slurp', 'duckduckbot', 'baiduspider',
'yandexbot', 'sogou', 'exabot',
// Social media bots
'facebot', 'facebookexternalhit', 'twitterbot', 'linkedinbot',
'whatsapp', 'telegrambot', 'slackbot', 'discordbot', 'pinterestbot',
// SEO tools
'applebot', 'semrushbot', 'ahrefsbot', 'mj12bot', 'dotbot',
'rogerbot', 'petalbot', 'screaming frog',
// Archive bots
'ia_archiver', 'archive.org_bot',
// Generic patterns
'crawler', 'spider', 'bot.htm', 'bot.php', 'crawl',
// HTTP clients
'wget', 'curl', 'python-requests', 'python/', 'scrapy',
'php/', 'java/', 'go-http-client', 'httpclient', 'apache-httpclient',
'libwww', 'lwp-', 'mechanize',
// Headless browsers
'headlesschrome', 'phantomjs', 'selenium', 'webdriver', 'puppeteer',
// API testing tools
'postman', 'insomnia',
// Monitoring
'pingdom', 'uptimerobot', 'monitoring', 'check_http',
// Generic http pattern (often bots)
'http',
'httrack',
'mediapartners',
];
foreach ($botPatterns as $pattern) {
if (strpos($userAgent, $pattern) !== false) {
return true;
}
}
return false;
}
/**
* Detect browser from user agent
*
* @param string|null $userAgent Optional user agent
* @return string Browser name (chrome, firefox, safari, edge, opera, ie, other)
*/
protected static function mprDetectBrowser($userAgent = null)
{
if ($userAgent === null) {
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
return 'other';
}
$userAgent = $_SERVER['HTTP_USER_AGENT'];
}
// Edge must be checked before Chrome (Edge contains "Chrome" in UA)
if (strpos($userAgent, 'Edg') !== false || strpos($userAgent, 'Edge') !== false) {
return 'edge';
}
// Chrome must be checked before Safari (Chrome contains "Safari" in UA)
if (strpos($userAgent, 'Chrome') !== false && strpos($userAgent, 'Chromium') === false) {
return 'chrome';
}
if (strpos($userAgent, 'Firefox') !== false) {
return 'firefox';
}
if (strpos($userAgent, 'Safari') !== false && strpos($userAgent, 'Chrome') === false) {
return 'safari';
}
if (strpos($userAgent, 'OPR') !== false || strpos($userAgent, 'Opera') !== false) {
return 'opera';
}
if (strpos($userAgent, 'MSIE') !== false || strpos($userAgent, 'Trident') !== false) {
return 'ie';
}
return 'other';
}
/**
* Detect device type from user agent
*
* @param string|null $userAgent Optional user agent
* @return string Device type (mobile, tablet, desktop)
*/
protected static function mprDetectDeviceType($userAgent = null)
{
if ($userAgent === null) {
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
return 'desktop';
}
$userAgent = $_SERVER['HTTP_USER_AGENT'];
}
$userAgentLower = strtolower($userAgent);
// Check mobile first (phones)
if (preg_match('/mobile|android.*mobile|iphone|ipod|phone|blackberry|iemobile|opera mini|opera mobi/i', $userAgentLower)) {
return 'mobile';
}
// Check tablet
if (preg_match('/tablet|ipad|android(?!.*mobile)|kindle|silk/i', $userAgentLower)) {
return 'tablet';
}
return 'desktop';
}
/**
* Detect operating system from user agent
*
* @param string|null $userAgent Optional user agent
* @return string OS name (windows, macos, android, ios, linux, other)
*/
protected static function mprDetectOS($userAgent = null)
{
if ($userAgent === null) {
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
return 'other';
}
$userAgent = $_SERVER['HTTP_USER_AGENT'];
}
if (strpos($userAgent, 'Windows') !== false) {
return 'windows';
}
if (strpos($userAgent, 'Mac OS X') !== false) {
return 'macos';
}
if (strpos($userAgent, 'Android') !== false) {
return 'android';
}
if (strpos($userAgent, 'iPhone') !== false || strpos($userAgent, 'iPad') !== false) {
return 'ios';
}
if (strpos($userAgent, 'Linux') !== false) {
return 'linux';
}
return 'other';
}
/**
* Generate session hash
*
* @param int $idGuest Guest ID
* @param string|null $extra Additional data to include in hash (e.g., campaign params)
* @param int $timeBucket Time bucket in seconds (default 30 minutes)
* @return string MD5 hash
*/
protected static function mprGenerateSessionHash($idGuest, $extra = null, $timeBucket = 1800)
{
$timestamp = floor(time() / $timeBucket);
$hashData = $idGuest . '_' . $timestamp;
if ($extra) {
$hashData .= '_' . $extra;
}
return md5($hashData);
}
/**
* Get current IP address
*
* @return string
*/
protected static function mprGetIPAddress()
{
return \Tools::getRemoteAddr();
}
/**
* Get current user agent
*
* @param int $maxLength Maximum length to return
* @return string|null
*/
protected static function mprGetUserAgent($maxLength = 512)
{
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
return null;
}
return substr($_SERVER['HTTP_USER_AGENT'], 0, $maxLength);
}
/**
* Detect current page type from controller
*
* @return array [page_type, page_id]
*/
protected static function mprDetectPageType()
{
$controller = \Tools::getValue('controller', '');
switch ($controller) {
case 'index':
return [self::$PAGE_TYPES['HOME'], null];
case 'category':
$pageId = (int) \Tools::getValue('id_category');
return [self::$PAGE_TYPES['CATEGORY'], $pageId ?: null];
case 'product':
$pageId = (int) \Tools::getValue('id_product');
return [self::$PAGE_TYPES['PRODUCT'], $pageId ?: null];
case 'cms':
$pageId = (int) \Tools::getValue('id_cms');
return [self::$PAGE_TYPES['CMS'], $pageId ?: null];
case 'cart':
return [self::$PAGE_TYPES['CART'], null];
case 'order':
case 'orderopc':
case 'checkout':
case 'mprexpresscheckoutcheckout':
return [self::$PAGE_TYPES['CHECKOUT'], null];
case 'orderconfirmation':
case 'order-confirmation':
$pageId = (int) \Tools::getValue('id_order');
return [self::$PAGE_TYPES['ORDER_CONFIRMATION'], $pageId ?: null];
case 'my-account':
case 'identity':
case 'addresses':
case 'address':
case 'history':
case 'order-detail':
case 'order-follow':
case 'order-slip':
return [self::$PAGE_TYPES['MY_ACCOUNT'], null];
case 'search':
return [self::$PAGE_TYPES['SEARCH'], null];
case 'manufacturer':
$pageId = (int) \Tools::getValue('id_manufacturer');
return [self::$PAGE_TYPES['MANUFACTURER'], $pageId ?: null];
case 'supplier':
$pageId = (int) \Tools::getValue('id_supplier');
return [self::$PAGE_TYPES['SUPPLIER'], $pageId ?: null];
case 'contact':
case 'contact-us':
return [self::$PAGE_TYPES['CONTACT'], null];
default:
return [self::$PAGE_TYPES['OTHER'], null];
}
}
/**
* Get page type name for display
*
* @param int $pageType Page type constant
* @return string
*/
public static function getPageTypeName($pageType)
{
$names = [
self::$PAGE_TYPES['HOME'] => 'Home',
self::$PAGE_TYPES['CATEGORY'] => 'Category',
self::$PAGE_TYPES['PRODUCT'] => 'Product',
self::$PAGE_TYPES['CMS'] => 'CMS',
self::$PAGE_TYPES['CART'] => 'Cart',
self::$PAGE_TYPES['CHECKOUT'] => 'Checkout',
self::$PAGE_TYPES['ORDER_CONFIRMATION'] => 'Order Confirmation',
self::$PAGE_TYPES['MY_ACCOUNT'] => 'My Account',
self::$PAGE_TYPES['SEARCH'] => 'Search',
self::$PAGE_TYPES['MANUFACTURER'] => 'Manufacturer',
self::$PAGE_TYPES['SUPPLIER'] => 'Supplier',
self::$PAGE_TYPES['CONTACT'] => 'Contact',
self::$PAGE_TYPES['OTHER'] => 'Other',
];
return $names[$pageType] ?? 'Unknown';
}
/**
* Get page types array
*
* @return array
*/
public static function getPageTypes()
{
return self::$PAGE_TYPES;
}
}