Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

10
classes/.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>

419
classes/Access.php Normal file
View File

@@ -0,0 +1,419 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Core\Security\Permission;
/**
* Class AccessCore.
*/
class AccessCore extends ObjectModel
{
/** @var int Profile id which address belongs to */
public $id_profile = null;
/** @var int AuthorizationRole id which address belongs to */
public $id_authorization_role = null;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'access',
'primary' => 'id_profile',
'fields' => [
'id_profile' => ['type' => self::TYPE_INT, 'validate' => 'isNullOrUnsignedId', 'copy_post' => false],
'id_authorization_role' => ['type' => self::TYPE_INT, 'validate' => 'isNullOrUnsignedId', 'copy_post' => false],
],
];
/**
* Is access granted to this Role?
*
* @param string|array<string> $role Role name ("Superadministrator", "sales", "translator", etc.)
* @param int $idProfile Profile ID
*
* @return bool Whether access is granted
*
* @throws Exception
*/
public static function isGranted($role, $idProfile)
{
foreach ((array) $role as $currentRole) {
preg_match(
'/ROLE_MOD_(?P<type>[A-Z]+)_(?P<name>[A-Z0-9_]+)_(?P<auth>[A-Z]+)/',
$currentRole,
$matches
);
if (isset($matches['type']) && $matches['type'] == 'TAB') {
$joinTable = _DB_PREFIX_ . 'access';
} elseif (isset($matches['type']) && $matches['type'] == 'MODULE') {
$joinTable = _DB_PREFIX_ . 'module_access';
} else {
throw new Exception('The slug ' . $currentRole . ' is invalid');
}
$currentRole = Db::getInstance()->escape($currentRole);
$isCurrentGranted = (bool) Db::getInstance()->getRow('
SELECT t.`id_authorization_role`
FROM `' . _DB_PREFIX_ . 'authorization_role` t
LEFT JOIN ' . $joinTable . ' j
ON j.`id_authorization_role` = t.`id_authorization_role`
WHERE `slug` = "' . $currentRole . '"
AND j.`id_profile` = "' . (int) $idProfile . '"
');
if (!$isCurrentGranted) {
return false;
}
}
return true;
}
/**
* Get all roles for the Profile ID.
*
* @param int $idProfile Profile ID
*
* @return array Roles
*/
public static function getRoles($idProfile)
{
$idProfile = (int) $idProfile;
$accesses = Db::getInstance()->executeS('
SELECT r.`slug`
FROM `' . _DB_PREFIX_ . 'authorization_role` r
INNER JOIN `' . _DB_PREFIX_ . 'access` a ON a.`id_authorization_role` = r.`id_authorization_role`
WHERE a.`id_profile` = "' . $idProfile . '"
');
$accessesFromModules = Db::getInstance()->executeS('
SELECT r.`slug`
FROM `' . _DB_PREFIX_ . 'authorization_role` r
INNER JOIN `' . _DB_PREFIX_ . 'module_access` ma ON ma.`id_authorization_role` = r.`id_authorization_role`
WHERE ma.`id_profile` = "' . $idProfile . '"
');
$roles = array_merge($accesses, $accessesFromModules);
foreach ($roles as $key => $role) {
$roles[$key] = $role['slug'];
}
return $roles;
}
/**
* Find Tab ID by slug.
*
* @param string $authSlug Slug
*
* @return string Tab ID
*
* @todo: Find out if we should return an int instead. (breaking change)
*/
public static function findIdTabByAuthSlug($authSlug)
{
preg_match(
'/ROLE_MOD_[A-Z]+_(?P<classname>[A-Z]+)_(?P<auth>[A-Z]+)/',
$authSlug,
$matches
);
$result = Db::getInstance()->getRow('
SELECT `id_tab`
FROM `' . _DB_PREFIX_ . 'tab`
WHERE UCASE(`class_name`) = "' . $matches['classname'] . '"
');
return $result['id_tab'];
}
/**
* Find slug by Tab ID.
*
* @param int $idTab Tab ID
*
* @return string Full module slug
*/
public static function findSlugByIdTab($idTab)
{
$result = Db::getInstance()->getRow('
SELECT `class_name`
FROM `' . _DB_PREFIX_ . 'tab`
WHERE `id_tab` = "' . (int) $idTab . '"
');
return self::sluggifyTab($result);
}
/**
* Find slug by Parent Tab ID.
*
* @param int $idParentTab Tab ID
*
* @return array<int, array<string, string>> Full module slug
*/
public static function findSlugByIdParentTab($idParentTab)
{
return Db::getInstance()->executeS('
SELECT `class_name`
FROM `' . _DB_PREFIX_ . 'tab`
WHERE `id_parent` = "' . (int) $idParentTab . '"
');
}
/**
* Find slug by Module ID.
*
* @param int $idModule Module ID
*
* @return string Full module slug
*/
public static function findSlugByIdModule($idModule)
{
$result = Db::getInstance()->getRow('
SELECT `name`
FROM `' . _DB_PREFIX_ . 'module`
WHERE `id_module` = "' . (int) $idModule . '"
');
return self::sluggifyModule($result);
}
/**
* Sluggify tab.
*
* @param array $tab Tab class name
* @param string $authorization 'CREATE'|'READ'|'UPDATE'|'DELETE'
*
* @return string Full slug for tab
*/
public static function sluggifyTab($tab, $authorization = '')
{
return sprintf('%s%s_%s', Permission::PREFIX_TAB, strtoupper($tab['class_name'] ?? ''), $authorization);
}
/**
* Sluggify module.
*
* @param array $module Module name
* @param string $authorization 'CREATE'|'READ'|'UPDATE'|'DELETE'
*
* @return string Full slug for module
*/
public static function sluggifyModule($module, $authorization = '')
{
return sprintf('%s%s_%s', Permission::PREFIX_MODULE, strtoupper($module['name'] ?? ''), $authorization);
}
/**
* Get legacy authorization.
*
* @param string $legacyAuth Legacy authorization
*
* @return bool|string|array Authorization
*/
public static function getAuthorizationFromLegacy($legacyAuth)
{
$auth = [
'add' => 'CREATE',
'view' => 'READ',
'edit' => 'UPDATE',
'configure' => 'UPDATE',
'delete' => 'DELETE',
'uninstall' => 'DELETE',
'duplicate' => ['CREATE', 'UPDATE'],
'all' => ['CREATE', 'READ', 'UPDATE', 'DELETE'],
];
return isset($auth[$legacyAuth]) ? $auth[$legacyAuth] : false;
}
/**
* Add access.
*
* @param int $idProfile Profile ID
* @param int $idRole Role ID
*
* @return string Whether access has been successfully granted ("ok", "error")
*/
public function addAccess($idProfile, $idRole)
{
$sql = '
INSERT IGNORE INTO `' . _DB_PREFIX_ . 'access` (`id_profile`, `id_authorization_role`)
VALUES (' . (int) $idProfile . ',' . (int) $idRole . ')
';
return Db::getInstance()->execute($sql) ? 'ok' : 'error';
}
/**
* Remove access.
*
* @param int $idProfile Profile ID
* @param int $idRole Role ID
*
* @return string Whether access has been successfully removed ("ok", "error")
*/
public function removeAccess($idProfile, $idRole)
{
$sql = '
DELETE FROM `' . _DB_PREFIX_ . 'access`
WHERE `id_profile` = "' . (int) $idProfile . '"
AND `id_authorization_role` = "' . (int) $idRole . '"
';
return Db::getInstance()->execute($sql) ? 'ok' : 'error';
}
/**
* Add module access.
*
* @param int $idProfile Profile ID
* @param int $idRole Role ID
*
* @return string Whether module access has been successfully granted ("ok", "error")
*/
public function addModuleAccess($idProfile, $idRole)
{
$sql = '
INSERT IGNORE INTO `' . _DB_PREFIX_ . 'module_access` (`id_profile`, `id_authorization_role`)
VALUES (' . (int) $idProfile . ',' . (int) $idRole . ')
';
return Db::getInstance()->execute($sql) ? 'ok' : 'error';
}
/**
* @param int $idProfile
* @param int $idRole
*
* @return string 'ok'|'error'
*/
public function removeModuleAccess($idProfile, $idRole)
{
$sql = '
DELETE FROM `' . _DB_PREFIX_ . 'module_access`
WHERE `id_profile` = "' . (int) $idProfile . '"
AND `id_authorization_role` = "' . (int) $idRole . '"
';
return Db::getInstance()->execute($sql) ? 'ok' : 'error';
}
/**
* Update legacy access.
*
* @param int $idProfile Profile ID
* @param int $idTab Tab ID
* @param string $lgcAuth Legacy authorization
* @param bool $enabled Whether access should be granted
* @param bool $addFromParent Child from parents
*
* @return string Whether legacy access has been successfully updated ("ok", "error")
*
* @throws Exception
*/
public function updateLgcAccess($idProfile, $idTab, $lgcAuth, $enabled, $addFromParent = true)
{
$idProfile = (int) $idProfile;
$idTab = (int) $idTab;
if ($idTab == -1) {
$slug = Permission::PREFIX_TAB . '%_';
} else {
$slug = self::findSlugByIdTab($idTab);
}
$whereClauses = [];
foreach ((array) self::getAuthorizationFromLegacy($lgcAuth) as $auth) {
$slugLike = Db::getInstance()->escape($slug . $auth);
$whereClauses[] = ' `slug` LIKE "' . $slugLike . '"';
}
if ($addFromParent) {
foreach (self::findSlugByIdParentTab($idTab) as $child) {
$child = self::sluggifyTab($child);
foreach ((array) self::getAuthorizationFromLegacy($lgcAuth) as $auth) {
$slugLike = Db::getInstance()->escape($child . $auth);
$whereClauses[] = ' `slug` LIKE "' . $slugLike . '"';
}
}
}
$roles = Db::getInstance()->executeS('
SELECT `id_authorization_role`
FROM `' . _DB_PREFIX_ . 'authorization_role` t
WHERE ' . implode(' OR ', $whereClauses) . '
');
if (empty($roles)) {
throw new Exception('Cannot find role slug');
}
$res = [];
foreach ($roles as $role) {
if ($enabled) {
$res[] = $this->addAccess($idProfile, $role['id_authorization_role']);
} else {
$res[] = $this->removeAccess($idProfile, $role['id_authorization_role']);
}
}
return in_array('error', $res) ? 'error' : 'ok';
}
/**
* Update (legacy) Module access.
*
* @param int $idProfile Profile ID
* @param int $idModule Module ID
* @param string $lgcAuth Legacy authorization
* @param bool $enabled Whether module access should be granted
*
* @return string Whether module access has been succesfully changed ("ok", "error")
*/
public function updateLgcModuleAccess($idProfile, $idModule, $lgcAuth, $enabled)
{
$idProfile = (int) $idProfile;
$idModule = (int) $idModule;
if ($idModule == -1) {
$slug = Permission::PREFIX_MODULE . '%_';
} else {
$slug = self::findSlugByIdModule($idModule);
}
$whereClauses = [];
foreach ((array) self::getAuthorizationFromLegacy($lgcAuth) as $auth) {
$slugLike = Db::getInstance()->escape($slug . $auth);
$whereClauses[] = ' `slug` LIKE "' . $slugLike . '"';
}
$roles = Db::getInstance()->executeS('
SELECT `id_authorization_role`
FROM `' . _DB_PREFIX_ . 'authorization_role` t
WHERE ' . implode(' OR ', $whereClauses) . '
');
$res = [];
foreach ($roles as $role) {
if ($enabled) {
$res[] = $this->addModuleAccess($idProfile, $role['id_authorization_role']);
} else {
$res[] = $this->removeModuleAccess($idProfile, $role['id_authorization_role']);
}
}
return in_array('error', $res) ? 'error' : 'ok';
}
}

654
classes/Address.php Normal file
View File

@@ -0,0 +1,654 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShopBundle\Form\Admin\Type\FormattedTextareaType;
/**
* Class AddressCore.
*/
class AddressCore extends ObjectModel
{
/** @var int Customer ID which address belongs to */
public $id_customer = null;
/** @var int Manufacturer ID which address belongs to */
public $id_manufacturer = null;
/** @var int Supplier ID which address belongs to */
public $id_supplier = null;
/** @var int Id warehouse the address belongs to
*
* @deprecated since 9.0, advanced stock management has been completely removed
*/
public $id_warehouse = 0;
/** @var int Country ID */
public $id_country;
/** @var int State ID */
public $id_state;
/** @var string Country name */
public $country;
/** @var string Alias (eg. Home, Work...) */
public $alias;
/** @var string Company (optional) */
public $company;
/** @var string Lastname */
public $lastname;
/** @var string Firstname */
public $firstname;
/** @var string Address first line */
public $address1;
/** @var string Address second line (optional) */
public $address2;
/** @var string Postal code */
public $postcode;
/** @var string City */
public $city;
/** @var string Any other useful information */
public $other;
/** @var string Phone number */
public $phone;
/** @var string Mobile phone number */
public $phone_mobile;
/** @var string VAT number */
public $vat_number;
/** @var string DNI number */
public $dni;
/** @var string Object creation date */
public $date_add;
/** @var string Object last modification date */
public $date_upd;
/** @var bool True if address has been deleted (staying in database as deleted) */
public $deleted = false;
/** @var int|null */
public $id_address;
/** @var array Zone IDs cache */
protected static $_idZones = [];
/** @var array Country IDs cache */
protected static $_idCountries = [];
/** @var array<int, bool> Store if an adress ID exists. Please note that soft-deleted address also belongs to this cache. */
protected static $addressExists = [];
/**
* @see ObjectModel::$definition
*/
// when you override this class, do not create a field with allow_null=>true
// because it will give you exception on checkout address step
public static $definition = [
'table' => 'address',
'primary' => 'id_address',
'fields' => [
'id_customer' => ['type' => self::TYPE_INT, 'validate' => 'isNullOrUnsignedId', 'copy_post' => false],
'id_manufacturer' => ['type' => self::TYPE_INT, 'validate' => 'isNullOrUnsignedId', 'copy_post' => false],
'id_supplier' => ['type' => self::TYPE_INT, 'validate' => 'isNullOrUnsignedId', 'copy_post' => false],
'id_warehouse' => ['type' => self::TYPE_INT, 'validate' => 'isNullOrUnsignedId', 'copy_post' => false],
'id_country' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_state' => ['type' => self::TYPE_INT, 'validate' => 'isNullOrUnsignedId'],
'alias' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 32],
'company' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 255],
'lastname' => ['type' => self::TYPE_STRING, 'validate' => 'isName', 'required' => true, 'size' => 255],
'firstname' => ['type' => self::TYPE_STRING, 'validate' => 'isName', 'required' => true, 'size' => 255],
'vat_number' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 32],
'address1' => ['type' => self::TYPE_STRING, 'validate' => 'isAddress', 'required' => true, 'size' => 128],
'address2' => ['type' => self::TYPE_STRING, 'validate' => 'isAddress', 'size' => 128],
'postcode' => ['type' => self::TYPE_STRING, 'validate' => 'isPostCode', 'size' => 12],
'city' => ['type' => self::TYPE_STRING, 'validate' => 'isCityName', 'required' => true, 'size' => 64],
'other' => ['type' => self::TYPE_STRING, 'validate' => 'isMessage', 'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4],
'phone' => ['type' => self::TYPE_STRING, 'validate' => 'isPhoneNumber', 'size' => 32],
'phone_mobile' => ['type' => self::TYPE_STRING, 'validate' => 'isPhoneNumber', 'size' => 32],
'dni' => ['type' => self::TYPE_STRING, 'validate' => 'isDniLite', 'size' => 16],
'deleted' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'copy_post' => false],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate', 'copy_post' => false],
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate', 'copy_post' => false],
],
];
/** @var array Web service parameters */
protected $webserviceParameters = [
'objectsNodeName' => 'addresses',
'fields' => [
'id_customer' => ['xlink_resource' => 'customers'],
'id_manufacturer' => ['xlink_resource' => 'manufacturers'],
'id_supplier' => ['xlink_resource' => 'suppliers'],
'id_country' => ['xlink_resource' => 'countries'],
'id_state' => ['xlink_resource' => 'states'],
],
];
/**
* Build an Address.
*
* @param int|null $id_address Existing Address ID in order to load object (optional)
* @param int|null $id_lang Language ID (optional). Configuration::PS_LANG_DEFAULT will be used if null
*/
public function __construct($id_address = null, $id_lang = null)
{
parent::__construct($id_address);
/* Get and cache address country name */
if ($this->id) {
$this->country = Country::getNameById($id_lang ? $id_lang : Configuration::get('PS_LANG_DEFAULT'), $this->id_country);
}
}
/**
* reset static cache (eg unit testing purpose).
*/
public static function resetStaticCache()
{
static::$_idZones = [];
static::$_idCountries = [];
static::$addressExists = [];
}
/**
* @see ObjectModel::add()
*/
public function add($autodate = true, $null_values = false)
{
if (!parent::add($autodate, $null_values)) {
return false;
}
if (Validate::isUnsignedId($this->id_customer)) {
Customer::resetAddressCache($this->id_customer, $this->id);
}
// Update the cache
static::$addressExists[$this->id] = true;
return true;
}
/**
* @see ObjectModel::update()
*/
public function update($null_values = false)
{
// Empty related caches
if (isset(self::$_idCountries[$this->id])) {
unset(self::$_idCountries[$this->id]);
}
if (isset(self::$_idZones[$this->id])) {
unset(self::$_idZones[$this->id]);
}
// Update the cache
// This is probably not correct, because it should be true only if the address is NOT flagged as deleted
static::$addressExists[$this->id] = true;
if (Validate::isUnsignedId($this->id_customer)) {
Customer::resetAddressCache($this->id_customer, $this->id);
}
/* Skip the required fields */
if ($this->isUsed()) {
self::$fieldsRequiredDatabase['Address'] = [];
}
return parent::update($null_values);
}
/**
* @see ObjectModel::delete()
*/
public function delete()
{
if (Validate::isUnsignedId($this->id_customer)) {
Customer::resetAddressCache($this->id_customer, $this->id);
}
/*
* Deleting an address can go two ways.
*
* 1) If the address is used in an order, we will only soft-delete it. This means mark it with a flag,
* hide it everywhere and prevent anyone using it. We must absolutely retain all the business data
* for the order.
* 2) If it's not used, we can safely delete the address.
*/
// First step is to unlink this address from all NON-ORDERED carts.
$this->deleteCartAddress();
// Second step - check if the address has been used in some order.
if (!$this->isUsed()) {
// If NO, we can safely delete it.
if (isset(static::$addressExists[$this->id])) {
static::$addressExists[$this->id] = false;
}
return parent::delete();
} else {
// If YES, we only soft delete it and keep it in the database.
return $this->softDelete();
}
}
/**
* Removes the address from all non ordered carts using it,
* to avoid errors on not existing address.
*/
protected function deleteCartAddress()
{
// Reset it from all delivery addresses
$sql = 'UPDATE ' . _DB_PREFIX_ . 'cart c
LEFT JOIN ' . _DB_PREFIX_ . 'orders o ON c.id_cart = o.id_cart
SET c.id_address_delivery = 0
WHERE c.id_address_delivery = ' . $this->id . ' AND o.id_order IS NULL';
Db::getInstance()->execute($sql);
// Reset it from all invoice addresses
$sql = 'UPDATE ' . _DB_PREFIX_ . 'cart c
LEFT JOIN ' . _DB_PREFIX_ . 'orders o ON c.id_cart = o.id_cart
SET c.id_address_invoice = 0
WHERE c.id_address_invoice = ' . $this->id . ' AND o.id_order IS NULL';
Db::getInstance()->execute($sql);
}
/**
* Get Zone ID for a given address.
*
* @param int $id_address Address ID for which we want to get the Zone ID
*
* @return int|bool Zone ID
*/
public static function getZoneById($id_address)
{
if (empty($id_address)) {
return false;
}
if (isset(self::$_idZones[$id_address])) {
return self::$_idZones[$id_address];
}
$id_zone = Hook::exec('actionGetIDZoneByAddressID', ['id_address' => $id_address]);
if (is_numeric($id_zone)) {
self::$_idZones[$id_address] = (int) $id_zone;
return self::$_idZones[$id_address];
}
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
SELECT s.`id_zone` AS id_zone_state, c.`id_zone`
FROM `' . _DB_PREFIX_ . 'address` a
LEFT JOIN `' . _DB_PREFIX_ . 'country` c ON c.`id_country` = a.`id_country`
LEFT JOIN `' . _DB_PREFIX_ . 'state` s ON s.`id_state` = a.`id_state`
WHERE a.`id_address` = ' . (int) $id_address);
if (empty($result['id_zone_state']) && empty($result['id_zone'])) {
return false;
}
self::$_idZones[$id_address] = !empty($result['id_zone_state'])
? (int) $result['id_zone_state']
: (int) $result['id_zone'];
return self::$_idZones[$id_address];
}
/**
* Check if the Country is active for a given address.
*
* @param int $id_address Address ID for which we want to get the Country status
*
* @return int|bool Country status
*/
public static function isCountryActiveById($id_address)
{
if (empty($id_address)) {
return false;
}
$cache_id = 'Address::isCountryActiveById_' . (int) $id_address;
if (!Cache::isStored($cache_id)) {
$result = (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
SELECT c.`active`
FROM `' . _DB_PREFIX_ . 'address` a
LEFT JOIN `' . _DB_PREFIX_ . 'country` c ON c.`id_country` = a.`id_country`
WHERE a.`id_address` = ' . (int) $id_address);
Cache::store($cache_id, $result);
return $result;
}
return Cache::retrieve($cache_id);
}
/**
* {@inheritdoc}
*/
public function validateField($field, $value, $id_lang = null, $skip = [], $human_errors = false)
{
$error = parent::validateField($field, $value, $id_lang, $skip, $human_errors);
if (true !== $error || 'dni' !== $field) {
return $error;
}
// Special validation for dni, check if the country needs it
if (!$this->deleted && static::dniRequired((int) $this->id_country) && Tools::isEmpty($value)) {
if ($human_errors) {
return $this->trans(
'The %s field is required.',
[$this->displayFieldName($field, get_class($this))],
'Admin.Notifications.Error'
);
}
return $this->trans(
'Property %s is empty.',
[get_class($this) . '->' . htmlspecialchars($field)],
'Admin.Notifications.Error'
);
}
return true;
}
/**
* Checks if DNI field is required for a given country.
*
* @param int $idCountry
*
* @return bool
*/
public static function dniRequired($idCountry)
{
return (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
'SELECT c.`need_identification_number` ' .
'FROM `' . _DB_PREFIX_ . 'country` c ' .
'WHERE c.`id_country` = ' . (int) $idCountry
);
}
/**
* Checks if the address has been used in an order as delivery on invoice address.
*
* @return int|bool Order count for this Address
*/
public function isUsed()
{
if ((int) $this->id <= 0) {
return false;
}
$result = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
SELECT COUNT(`id_order`) AS used
FROM `' . _DB_PREFIX_ . 'orders`
WHERE `id_address_delivery` = ' . (int) $this->id . '
OR `id_address_invoice` = ' . (int) $this->id);
return $result > 0 ? (int) $result : false;
}
/**
* Get Country and State of this Address.
*
* @param int $id_address Address ID
*
* @return array|bool
*/
public static function getCountryAndState($id_address)
{
if (isset(self::$_idCountries[$id_address])) {
return self::$_idCountries[$id_address];
}
if ($id_address) {
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
SELECT `id_country`, `id_state`, `vat_number`, `postcode` FROM `' . _DB_PREFIX_ . 'address`
WHERE `id_address` = ' . (int) $id_address);
} else {
$result = false;
}
self::$_idCountries[$id_address] = $result;
return $result;
}
/**
* Specify if an address is already in database.
* Please note that a soft-deleted address also counts as existing.
*
* @param int $id_address Address id
* @param bool $useCache Use Cache for optimizing queries
*
* @return bool The address exists
*/
public static function addressExists($id_address, bool $useCache = false)
{
if ((int) $id_address <= 0) {
return false;
}
if ($useCache && isset(static::$addressExists[$id_address])) {
return static::$addressExists[$id_address];
}
static::$addressExists[$id_address] = (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
'SELECT `id_address`
FROM ' . _DB_PREFIX_ . 'address a
WHERE a.`id_address` = ' . (int) $id_address,
false
);
return static::$addressExists[$id_address];
}
/**
* Check if the address is valid - active and not deleted.
*
* @param int $id_address Address id
*
* @return bool The address is valid
*/
public static function isValid($id_address)
{
$id_address = (int) $id_address;
$isValid = Db::getInstance()->getValue('
SELECT `id_address` FROM ' . _DB_PREFIX_ . 'address a
WHERE a.`id_address` = ' . $id_address . ' AND a.`deleted` = 0 AND a.`active` = 1
');
return (bool) $isValid;
}
/**
* Get the first address id of the customer.
*
* @param int $id_customer Customer id
* @param bool $active Active addresses only
*
* @return bool|int|null
*/
public static function getFirstCustomerAddressId($id_customer, $active = true)
{
if (!$id_customer) {
return false;
}
$cache_id = 'Address::getFirstCustomerAddressId_' . (int) $id_customer . '-' . (bool) $active;
if (!Cache::isStored($cache_id)) {
$result = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
'
SELECT `id_address`
FROM `' . _DB_PREFIX_ . 'address`
WHERE `id_customer` = ' . (int) $id_customer . ' AND `deleted` = 0' . ($active ? ' AND `active` = 1' : '') . '
ORDER BY `id_address` ASC'
);
Cache::store($cache_id, $result);
return $result;
}
return Cache::retrieve($cache_id);
}
/**
* Initialize an address corresponding to the specified id address or if empty to the
* default shop configuration.
*
* @param int $id_address
* @param bool $with_geoloc
*
* @return Address address
*
* @throws PrestaShopException
*/
public static function initialize($id_address = null, $with_geoloc = false)
{
$context = Context::getContext();
/*
* To save performance, we cache the result, here we resolve a unique hash
* depending on the parameters used to initialize the address.
*/
if ($id_address) {
$context_hash = (int) $id_address;
} elseif ($with_geoloc && isset($context->customer->geoloc_id_country)) {
$context_hash = md5((int) $context->customer->geoloc_id_country . '-' . (int) $context->customer->id_state . '-' . $context->customer->postcode);
} else {
$context_hash = md5((string) $context->country->id);
}
$cache_id = 'Address::initialize_' . $context_hash;
if (!Cache::isStored($cache_id)) {
/*
* Case 1 - Specific address ID is provided. We just load it.
*/
if ($id_address) {
$address = new Address((int) $id_address);
if (!Validate::isLoadedObject($address)) {
throw new PrestaShopException('Invalid address #' . (int) $id_address);
}
/*
* Case 2 - We try to use geolocation information if allowed. However, geoloc_id_country
* is not set to the customer anywhere in the current codebase. Most geolocation logic either
* native or from modules depend on setting the country directly in the context. So, this will
* probably never work.
*/
} elseif ($with_geoloc && isset($context->customer->geoloc_id_country)) {
$address = new Address();
$address->id_country = (int) $context->customer->geoloc_id_country;
$address->id_state = (int) $context->customer->id_state;
$address->postcode = $context->customer->postcode;
/*
* Case 3 - There is already a country set in the context and it's not the shop country.
* This is pretty common if you either use geolocation modules or let the user select his country
* via a custom country selector on the front office.
*/
} elseif ((int) $context->country->id && ((int) $context->country->id != Configuration::get('PS_SHOP_COUNTRY_ID'))) {
$address = new Address();
$address->id_country = (int) $context->country->id;
$address->id_state = 0;
$address->postcode = '0';
/*
* Case 4 - If there is no country in the context, or the country is the same, we use the shop country.
* We also assign the state and postcode if available.
*/
} elseif ((int) Configuration::get('PS_SHOP_COUNTRY_ID')) {
// set the default address
$address = new Address();
$address->id_country = (int) Configuration::get('PS_SHOP_COUNTRY_ID');
$address->id_state = (int) Configuration::get('PS_SHOP_STATE_ID');
$address->postcode = Configuration::get('PS_SHOP_CODE');
/*
* Case 5 - As a last resort, we just use the default country. It will most likely be the shop country anyway.
*/
} else {
// set the default address
$address = new Address();
$address->id_country = (int) Configuration::get('PS_COUNTRY_DEFAULT');
$address->id_state = 0;
$address->postcode = '0';
}
// Store it in cache, so we don't have to re-initialize it again
Cache::store($cache_id, $address);
return $address;
}
return Cache::retrieve($cache_id);
}
/**
* Returns Address ID for a given Supplier ID.
*
* @param int $id_supplier Supplier ID
*
* @return int $id_address Address ID
*/
public static function getAddressIdBySupplierId($id_supplier)
{
$query = new DbQuery();
$query->select('id_address');
$query->from('address');
$query->where('id_supplier = ' . (int) $id_supplier);
$query->where('deleted = 0');
$query->where('id_customer = 0');
$query->where('id_manufacturer = 0');
return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query);
}
/**
* Check if the alias already exists.
*
* @param string $alias Alias of an address
* @param int $id_address Address id
* @param int $id_customer Customer id
*
* @return false|string|null Amount of aliases found
*
* @todo: Find out if we shouldn't be returning an int instead? (breaking change)
*/
public static function aliasExist($alias, $id_address, $id_customer)
{
$query = new DbQuery();
$query->select('count(*)');
$query->from('address');
$query->where('alias = \'' . pSQL($alias) . '\'');
$query->where('id_address != ' . (int) $id_address);
$query->where('id_customer = ' . (int) $id_customer);
$query->where('deleted = 0');
return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query, false);
}
/**
* @see ObjectModel::getFieldsRequiredDB();
*/
public function getFieldsRequiredDB()
{
return parent::getCachedFieldsRequiredDatabase();
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class AddressChecksumCore.
*/
class AddressChecksumCore implements ChecksumInterface
{
public const SEPARATOR = '_';
/**
* Generate a checksum.
*
* @param Address $address
*
* @return string SHA1 checksum for the Address
*/
public function generateChecksum($address)
{
if (!$address->id) {
return sha1('No address set');
}
$uniqId = '';
try {
$fields = $address->getFields();
} catch (Exception $e) {
return sha1('Invalid address');
}
foreach ($fields as $name => $value) {
$uniqId .= $value . self::SEPARATOR;
}
$uniqId = rtrim($uniqId, self::SEPARATOR);
return sha1($uniqId);
}
}

651
classes/AddressFormat.php Normal file
View File

@@ -0,0 +1,651 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Core\Domain\Address\Exception\AddressException;
/**
* Class AddressFormatCore.
*/
class AddressFormatCore extends ObjectModel
{
public const FORMAT_NEW_LINE = "\n";
/** @var int Address format */
public $id_address_format;
/** @var int Country ID */
public $id_country;
/** @var string Format */
public $format;
protected $_errorFormatList = [];
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'address_format',
'primary' => 'id_country',
'fields' => [
'format' => ['type' => self::TYPE_HTML, 'validate' => 'isGenericName', 'required' => true, 'size' => 255],
'id_country' => ['type' => self::TYPE_INT],
],
];
/** @var array Default required form fields list */
public static $requireFormFieldsList = [
'firstname',
'lastname',
'address1',
'city',
'Country:name',
];
/** @var array Default forbidden property list */
public static $forbiddenPropertyList = [
'deleted',
'date_add',
'alias',
'secure_key',
'note',
'newsletter',
'ip_registration_newsletter',
'newsletter_date_add',
'optin',
'passwd',
'last_passwd_gen',
'active',
'is_guest',
'date_upd',
'country',
'years',
'days',
'months',
'description',
'meta_description',
'short_description',
'link_rewrite',
'meta_title',
'display_tax_label',
'need_zip_code',
'contains_states',
'call_prefixes',
'show_public_prices',
'max_payment',
'max_payment_days',
'geoloc_postcode',
'logged',
'account_number',
'groupBox',
'ape',
'max_payment',
'outstanding_allow_amount',
'call_prefix',
'definition',
'debug_list',
];
/** @var array Default formbidden class list */
public static $forbiddenClassList = [
'Manufacturer',
'Supplier',
];
public const _CLEANING_REGEX_ = '#([^\w:_]+)#i';
/**
* Check if the the association of the field name and a class name
* is valid.
*
* @param string $className The name class
* @param string $fieldName The property name
* @param bool $isIdField Do we have to allow a property name to be started with 'id_'
*
* @return bool Association of the field and class name is valid
*/
protected function _checkValidateClassField($className, $fieldName, $isIdField)
{
$isValid = false;
if (!class_exists($className)) {
$this->_errorFormatList[] = $this->trans('This class name does not exist.', [], 'Admin.Notifications.Error') .
': ' . $className;
} else {
$obj = new $className();
$reflect = new ReflectionObject($obj);
// Check if the property is accessible
$publicProperties = $reflect->getProperties(ReflectionProperty::IS_PUBLIC);
foreach ($publicProperties as $property) {
$propertyName = $property->getName();
if (($propertyName == $fieldName) && ($isIdField
|| (!preg_match('/\bid\b|id_\w+|\bid[A-Z]\w+/', $propertyName)))) {
$isValid = true;
}
}
if (!$isValid) {
$this->_errorFormatList[] = $this->trans('This property does not exist in the class or is forbidden.', [], 'Admin.Notifications.Error') .
': ' . $className . ': ' . $fieldName;
}
unset(
$obj,
$reflect
);
}
return $isValid;
}
/**
* Verify the existence of a field name and check the availability
* of an association between a field name and a class (ClassName:fieldName)
* if the separator is overview.
*
* @param string $patternName The composition of the class and field name
*/
protected function _checkLiableAssociation($patternName)
{
$patternName = trim($patternName);
$associationName = explode(':', $patternName);
$totalNameUsed = count($associationName);
if ($totalNameUsed > 2) {
$this->_errorFormatList[] = $this->trans('This association has too many elements.', [], 'Admin.Notifications.Error');
} elseif ($totalNameUsed == 1) {
$associationName[0] = strtolower($associationName[0]);
if (in_array($associationName[0], self::$forbiddenPropertyList)
|| !$this->_checkValidateClassField('Address', $associationName[0], false)) {
$this->_errorFormatList[] = $this->trans('This name is not allowed.', [], 'Admin.Notifications.Error') . ': ' .
$associationName[0];
}
} else {
if (empty($associationName[0]) || empty($associationName[1])) {
$this->_errorFormatList[] = $this->trans('Syntax error with this pattern.', [], 'Admin.Notifications.Error') . ': ' . $patternName;
} else {
$associationName[0] = ucfirst($associationName[0]);
$associationName[1] = strtolower($associationName[1]);
if (in_array($associationName[0], self::$forbiddenClassList)) {
$this->_errorFormatList[] = $this->trans('This name is not allowed.', [], 'Admin.Notifications.Error') . ': ' .
$associationName[0];
} else {
// Check if the id field name exist in the Address class
// Don't check this attribute on Address (no sense)
if ($associationName[0] != 'Address') {
$this->_checkValidateClassField('Address', 'id_' . strtolower($associationName[0]), true);
}
// Check if the field name exist in the class write by the user
$this->_checkValidateClassField($associationName[0], $associationName[1], false);
}
}
}
}
/**
* Check if the set fields are valid.
*/
public function checkFormatFields()
{
$this->_errorFormatList = [];
$usedKeyList = [];
$multipleLineFields = explode(self::FORMAT_NEW_LINE, $this->format);
foreach ($multipleLineFields as $lineField) {
if ($patternsName = preg_split(self::_CLEANING_REGEX_, $lineField, -1, PREG_SPLIT_NO_EMPTY)) {
if (is_array($patternsName)) {
foreach ($patternsName as $patternName) {
if (!in_array($patternName, $usedKeyList)) {
$this->_checkLiableAssociation($patternName);
$usedKeyList[] = $patternName;
} else {
$this->_errorFormatList[] = $this->trans('This key has already been used.', [], 'Admin.Notifications.Error') .
': ' . $patternName;
}
}
}
}
}
$this->checkRequiredFields($usedKeyList);
return !count($this->_errorFormatList);
}
/**
* Checks that all required fields exist in a given fields list.
* Fills _errorFormatList array in case of absence of a required field.
*
* @param array $fieldList
*/
protected function checkRequiredFields($fieldList)
{
foreach (self::getFieldsRequired() as $requiredField) {
if (!in_array($requiredField, $fieldList)) {
$this->_errorFormatList[] = $this->trans(
'The %s field (in tab %s) is required.',
[htmlspecialchars($requiredField), htmlspecialchars($this->getFieldTabName($requiredField))],
'Admin.Notifications.Error');
}
}
}
/**
* Given a field name, get the name of the tab in which the field name can be found.
* For ex: Country:name => the tab is 'Country'.
* There should be only one separator in the string, otherwise throw an exception.
*
* @param string $field
*
* @return string
*
* @throws AddressException
*/
private function getFieldTabName($field)
{
if (strpos($field, ':') === false) {
// When there is no ':' separator, the field is in the Address tab
return 'Address';
}
$fieldTab = explode(':', $field);
if (count($fieldTab) === 2) {
// The part preceding the ':' separator is the name of the tab in which there is the required field
return $fieldTab[0];
}
throw new AddressException('Address format field is not valid');
}
/**
* Returns the error list.
*/
public function getErrorList()
{
return $this->_errorFormatList;
}
/**
* Set the layout key with the liable value
* example : (firstname) => 'Presta' will result (Presta)
* : (firstname-lastname) => 'Presta' and 'Shop' result '(Presta-Shop)'.
*/
protected static function _setOriginalDisplayFormat(&$formattedValueList, $currentLine, $currentKeyList)
{
if ($currentKeyList && is_array($currentKeyList)) {
$originalFormattedPatternList = explode(' ', $currentLine);
// Foreach the available pattern
foreach ($originalFormattedPatternList as $patternNum => $pattern) {
// Var allows to modify the good formatted key value when multiple key exist into the same pattern
$mainFormattedKey = '';
// Multiple key can be found in the same pattern
foreach ($currentKeyList as $key) {
// Check if we need to use an older modified pattern if a key has already be matched before
$replacedValue = empty($mainFormattedKey) ? $pattern : $formattedValueList[$mainFormattedKey];
$chars = $start = $end = str_replace($key, '', $replacedValue);
if (preg_match(self::_CLEANING_REGEX_, $chars)) {
if (Tools::substr($replacedValue, 0, Tools::strlen($chars)) == $chars) {
$end = '';
} else {
$start = '';
}
if ($chars) {
$replacedValue = str_replace($chars, '', $replacedValue);
}
}
if (empty($formattedValueList[$key])) {
return;
}
$formattedValue = preg_replace('/^' . $key . '$/', $formattedValueList[$key], $replacedValue, -1, $count);
if ($formattedValue) {
if ($count) {
// Allow to check multiple key in the same pattern,
if (empty($mainFormattedKey)) {
$mainFormattedKey = $key;
}
// Set the pattern value to an empty string if an older key has already been matched before
if ($mainFormattedKey != $key) {
$formattedValueList[$key] = '';
}
// Store the new pattern value
$formattedValueList[$mainFormattedKey] = $start . $formattedValue . $end;
unset($originalFormattedPatternList[$patternNum]);
}
}
}
}
}
}
/**
* Cleaned the layout set by the user.
*/
public static function cleanOrderedAddress(&$orderedAddressField)
{
foreach ($orderedAddressField as &$line) {
$cleanedLine = '';
if ($keyList = preg_split(self::_CLEANING_REGEX_, $line, -1, PREG_SPLIT_NO_EMPTY)) {
foreach ($keyList as $key) {
$cleanedLine .= $key . ' ';
}
$cleanedLine = trim($cleanedLine);
$line = $cleanedLine;
}
}
}
/**
* Returns the formatted fields with associated values.
*
* @param Address $address Address object
* @param array $addressFormat Address format fields by line
* @param int|null $id_lang
*
* @return array
*/
public static function getFormattedAddressFieldsValues($address, $addressFormat, $id_lang = null)
{
if (!$id_lang) {
$id_lang = Context::getContext()->language->id;
}
$tab = [];
$temporyObject = [];
// Check if $address exist and it's an instanciate object of Address
if ($address instanceof Address) {
foreach ($addressFormat as $line) {
if (($keyList = preg_split(self::_CLEANING_REGEX_, $line, -1, PREG_SPLIT_NO_EMPTY)) && is_array($keyList)) {
foreach ($keyList as $pattern) {
$associateName = explode(':', $pattern);
$totalName = count($associateName);
if ($totalName == 1 && isset($address->{$associateName[0]})) {
$tab[$associateName[0]] = $address->{$associateName[0]};
} else {
$tab[$pattern] = '';
// Check if the property exist in both classes
if (($totalName == 2) && class_exists($associateName[0])
&& property_exists($associateName[0], $associateName[1])
&& property_exists($address, 'id_' . strtolower($associateName[0]))) {
$idFieldName = 'id_' . strtolower($associateName[0]);
if (!isset($temporyObject[$associateName[0]])) {
$temporyObject[$associateName[0]] = new $associateName[0]($address->{$idFieldName});
}
$tab[$pattern] = is_array($temporyObject[$associateName[0]]->{$associateName[1]}) ?
(
isset($temporyObject[$associateName[0]]->{$associateName[1]}[$id_lang]) ?
$temporyObject[$associateName[0]]->{$associateName[1]}[$id_lang] :
''
) :
$temporyObject[$associateName[0]]->{$associateName[1]};
}
}
}
AddressFormat::_setOriginalDisplayFormat($tab, $line, $keyList);
}
}
}
AddressFormat::cleanOrderedAddress($addressFormat);
return $tab;
}
/**
* Generates the full address text.
*
* @param Address $address
* @param array $patternRules A defined rules array to avoid some pattern
* @param string $newLine A string containing the newLine format
* @param string $separator A string containing the separator format
* @param array $style
*
* @return string
*/
public static function generateAddress(Address $address, $patternRules = [], $newLine = self::FORMAT_NEW_LINE, $separator = ' ', $style = [])
{
$addressFields = AddressFormat::getOrderedAddressFields($address->id_country);
$addressFormatedValues = AddressFormat::getFormattedAddressFieldsValues($address, $addressFields);
$addressText = '';
foreach ($addressFields as $line) {
if ($patternsList = preg_split(self::_CLEANING_REGEX_, $line, -1, PREG_SPLIT_NO_EMPTY)) {
$tmpText = '';
foreach ($patternsList as $pattern) {
if (!array_key_exists('avoid', $patternRules) || !in_array($pattern, $patternRules['avoid'])) {
$tmpText .= (isset($addressFormatedValues[$pattern]) && !empty($addressFormatedValues[$pattern])) ?
(((isset($style[$pattern])) ?
(sprintf($style[$pattern], $addressFormatedValues[$pattern])) :
$addressFormatedValues[$pattern]) . $separator) : '';
}
}
$tmpText = trim($tmpText);
$addressText .= (!empty($tmpText)) ? $tmpText . $newLine : '';
}
}
$addressText = preg_replace('/' . preg_quote($newLine, '/') . '$/i', '', $addressText);
$addressText = rtrim($addressText, $separator);
return $addressText;
}
/**
* Generate formatted Address string for display on Smarty templates.
*
* @param array $params Address parameters
* @param Smarty $smarty Smarty instance
*
* @return string Formatted Address string
*/
public static function generateAddressSmarty($params, &$smarty)
{
return AddressFormat::generateAddress(
$params['address'],
isset($params['patternRules']) ? $params['patternRules'] : [],
isset($params['newLine']) ? $params['newLine'] : self::FORMAT_NEW_LINE,
isset($params['separator']) ? $params['separator'] : ' ',
isset($params['style']) ? $params['style'] : []
);
}
/**
* Returns selected fields required for an address in an array according to a selection hash.
*
* @return array String values
*/
public static function getValidateFields($className)
{
$propertyList = [];
if (class_exists($className)) {
$object = new $className();
$reflect = new ReflectionObject($object);
// Check if the property is accessible
$publicProperties = $reflect->getProperties(ReflectionProperty::IS_PUBLIC);
foreach ($publicProperties as $property) {
$propertyName = $property->getName();
if ((!in_array($propertyName, AddressFormat::$forbiddenPropertyList))
&& (!preg_match('#id|id_\w#', $propertyName))) {
$propertyList[] = $propertyName;
}
}
unset(
$object,
$reflect
);
}
return $propertyList;
}
/**
* Return a list of liable class of the className.
*
* @param string $className
*
* @return array
*/
public static function getLiableClass($className)
{
$objectList = [];
if (class_exists($className)) {
$object = new $className();
$reflect = new ReflectionObject($object);
// Get all the name object liable to the Address class
$publicProperties = $reflect->getProperties(ReflectionProperty::IS_PUBLIC);
foreach ($publicProperties as $property) {
$propertyName = $property->getName();
if (preg_match('#id_\w#', $propertyName) && strlen($propertyName) > 3) {
$nameObject = ucfirst(substr($propertyName, 3));
if (!in_array($nameObject, self::$forbiddenClassList)
&& class_exists($nameObject)) {
$objectList[$nameObject] = new $nameObject();
}
}
}
unset(
$object,
$reflect
);
}
return $objectList;
}
/**
* Returns address format fields in array by country.
*
* @param int $idCountry If null using PS_COUNTRY_DEFAULT
* @param bool $splitAll
* @param bool $cleaned
*
* @return array String field address format
*/
public static function getOrderedAddressFields($idCountry = 0, $splitAll = false, $cleaned = false)
{
$out = [];
$fieldSet = explode(AddressFormat::FORMAT_NEW_LINE, AddressFormat::getAddressCountryFormat($idCountry));
foreach ($fieldSet as $fieldItem) {
if ($splitAll) {
$keyList = $cleaned ? preg_split(self::_CLEANING_REGEX_, $fieldItem, -1, PREG_SPLIT_NO_EMPTY) : explode(' ', $fieldItem);
foreach ($keyList as $wordItem) {
$out[] = trim($wordItem);
}
} else {
$out[] = ($cleaned) ? implode(' ', preg_split(self::_CLEANING_REGEX_, trim($fieldItem), -1, PREG_SPLIT_NO_EMPTY))
: trim($fieldItem);
}
}
return $out;
}
/**
* Return a data array containing ordered, formatedValue and object fields.
*/
public static function getFormattedLayoutData($address)
{
$layoutData = [];
if ($address && $address instanceof Address) {
$layoutData['ordered'] = AddressFormat::getOrderedAddressFields((int) $address->id_country);
$layoutData['formated'] = AddressFormat::getFormattedAddressFieldsValues($address, $layoutData['ordered']);
$layoutData['object'] = [];
$reflect = new ReflectionObject($address);
$publicProperties = $reflect->getProperties(ReflectionProperty::IS_PUBLIC);
foreach ($publicProperties as $property) {
if (isset($address->{$property->getName()})) {
$layoutData['object'][$property->getName()] = $address->{$property->getName()};
}
}
}
return $layoutData;
}
/**
* Returns address format by country if not defined using default country.
*
* @param int $idCountry Country ID
*
* @return string field address format
*/
public static function getAddressCountryFormat($idCountry = 0)
{
$idCountry = (int) $idCountry;
$tmpObj = new AddressFormat();
$tmpObj->id_country = $idCountry;
$out = $tmpObj->getFormat($tmpObj->id_country);
unset($tmpObj);
return $out;
}
/**
* Returns address format by Country.
*
* @param int $idCountry Country ID
*
* @return string field Address format
*/
public function getFormat($idCountry)
{
$out = $this->getFormatDB($idCountry);
if (empty($out)) {
$out = $this->getFormatDB((int) Configuration::get('PS_COUNTRY_DEFAULT'));
}
if (Country::isNeedDniByCountryId($idCountry) && false === strpos($out, 'dni')) {
$out .= AddressFormat::FORMAT_NEW_LINE . 'dni';
}
return $out;
}
/**
* Get Address format from DB.
*
* @param int $idCountry Country ID
*
* @return false|string|null Address format
*/
protected function getFormatDB($idCountry)
{
if (!Cache::isStored('AddressFormat::getFormatDB' . $idCountry)) {
$format = Db::getInstance()->getValue('
SELECT format
FROM `' . _DB_PREFIX_ . $this->def['table'] . '`
WHERE `id_country` = ' . (int) $idCountry);
$format = trim($format);
Cache::store('AddressFormat::getFormatDB' . $idCountry, $format);
return $format;
}
return Cache::retrieve('AddressFormat::getFormatDB' . $idCountry);
}
/**
* @see ObjectModel::getFieldsRequired()
*/
public static function getFieldsRequired()
{
$address = new CustomerAddress();
return array_unique(array_merge($address->getFieldsRequiredDB(), AddressFormat::$requireFormFieldsList));
}
}

145
classes/Alias.php Normal file
View File

@@ -0,0 +1,145 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class AliasCore.
*/
class AliasCore extends ObjectModel
{
public $alias;
public $search;
public $active = true;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'alias',
'primary' => 'id_alias',
'fields' => [
'search' => ['type' => self::TYPE_STRING, 'validate' => 'isValidSearch', 'required' => true, 'size' => 255],
'alias' => ['type' => self::TYPE_STRING, 'validate' => 'isValidSearch', 'required' => true, 'size' => 191],
'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
],
];
/**
* AliasCore constructor.
*
* @param int|null $id Alias ID
* @param string|null $alias Alias
* @param string|null $search Search string
*/
public function __construct($id = null, $alias = null, $search = null)
{
$this->def = Alias::getDefinition($this);
if ($id) {
parent::__construct($id);
} elseif ($alias && Validate::isValidSearch($alias)) {
if (!Alias::isFeatureActive()) {
$this->alias = trim($alias);
$this->search = trim($search);
} else {
$row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
SELECT a.id_alias, a.search, a.alias
FROM `' . _DB_PREFIX_ . 'alias` a
WHERE `alias` = \'' . pSQL($alias) . '\' AND `active` = 1');
if ($row) {
$this->id = (int) $row['id_alias'];
$this->search = $search ? trim($search) : $row['search'];
$this->alias = $row['alias'];
} else {
$this->alias = trim($alias);
$this->search = trim($search);
}
}
}
}
/**
* @see ObjectModel::add();
*/
public function add($autoDate = true, $nullValues = false)
{
$this->alias = Tools::replaceAccentedChars($this->alias);
$this->search = Tools::replaceAccentedChars($this->search);
if (parent::add($autoDate, $nullValues)) {
// Set cache of feature detachable to true
Configuration::updateGlobalValue('PS_ALIAS_FEATURE_ACTIVE', '1');
return true;
}
return false;
}
/**
* @see ObjectModel::delete();
*/
public function delete()
{
if (parent::delete()) {
// Refresh cache of feature detachable
Configuration::updateGlobalValue('PS_ALIAS_FEATURE_ACTIVE', Alias::isCurrentlyUsed($this->def['table'], true));
return true;
}
return false;
}
/**
* Get all found aliases from DB with search query.
*
* @return string Comma separated aliases
*/
public function getAliases()
{
if (!Alias::isFeatureActive()) {
return '';
}
$aliases = Db::getInstance()->executeS('
SELECT a.alias
FROM `' . _DB_PREFIX_ . 'alias` a
WHERE `search` = \'' . pSQL($this->search) . '\'');
$aliases = array_map('implode', $aliases);
return implode(', ', $aliases);
}
/**
* This method is allowed to know if a feature is used or active.
*
* @return bool
*/
public static function isFeatureActive()
{
return Configuration::get('PS_ALIAS_FEATURE_ACTIVE');
}
/**
* This method is allowed to know if an alias exist for AdminImportController.
*
* @param int $idAlias Alias ID
*
* @return bool
*/
public static function aliasExists($idAlias)
{
$sql = new DbQuery();
$sql->select('a.`id_alias`');
$sql->from('alias', 'a');
$sql->where('a.`id_alias` = ' . (int) $idAlias);
$row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql, false);
return isset($row['id_alias']);
}
}

339
classes/Attachment.php Normal file
View File

@@ -0,0 +1,339 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShopBundle\Form\Admin\Type\FormattedTextareaType;
/**
* Class AttachmentCore.
*/
class AttachmentCore extends ObjectModel
{
public $file;
public $file_name;
public $file_size;
public $name;
public $mime;
public $description;
/** @var int position Position */
public $position;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'attachment',
'primary' => 'id_attachment',
'multilang' => true,
'fields' => [
'file' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 40],
'mime' => ['type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'required' => true, 'size' => 128],
'file_name' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 255],
'file_size' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
/* Lang fields */
'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 255],
'description' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isCleanHtml', 'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4],
],
'associations' => [
'products' => ['type' => self::HAS_MANY, 'field' => 'id_product', 'object' => 'Product', 'association' => 'product_attachment'],
],
];
protected $webserviceParameters = [
'objectNodeNames' => 'attachments',
'hidden_fields' => [],
'fields' => [
'file' => [],
'file_name' => [],
'file_size' => [],
'mime' => [],
],
'associations' => [
'products' => [
'resource' => 'product',
'api' => 'products',
'fields' => [
'id' => ['required' => true],
],
],
],
];
/**
* @see ObjectModel::add()
*/
public function add($autoDate = true, $nullValues = false)
{
if (file_exists(_PS_DOWNLOAD_DIR_ . $this->file)) {
$this->file_size = filesize(_PS_DOWNLOAD_DIR_ . $this->file);
}
return parent::add($autoDate, $nullValues);
}
/**
* @see ObjectModel::update()
*/
public function update($nullValues = false)
{
if (file_exists(_PS_DOWNLOAD_DIR_ . $this->file)) {
$this->file_size = filesize(_PS_DOWNLOAD_DIR_ . $this->file);
}
return parent::update($nullValues);
}
/**
* @see ObjectModel::delete()
*/
public function delete()
{
if (file_exists(_PS_DOWNLOAD_DIR_ . $this->file)) {
@unlink(_PS_DOWNLOAD_DIR_ . basename($this->file));
}
$sql = new DbQuery();
$sql->select('pa.`id_product`');
$sql->from('product_attachment', 'pa');
$sql->where('pa.`id_attachment` = ' . (int) $this->id);
$products = Db::getInstance()->executeS($sql);
Db::getInstance()->delete(
'product_attachment',
'`id_attachment` = ' . (int) $this->id
);
foreach ($products as $product) {
Product::updateCacheAttachment((int) $product['id_product']);
}
return parent::delete();
}
/**
* Delete selection of attachments.
*
* @param array $attachments Attachments
*
* @return bool|int Whether the selection has been successfully deleted
*
* @todo: Find out if $return can be initialized with true. (breaking change)
*/
public function deleteSelection(array $attachments)
{
$return = 1;
foreach ($attachments as $idAttachment) {
$attachment = new Attachment((int) $idAttachment);
$return &= $attachment->delete();
}
return $return;
}
/**
* Get attachments.
*
* @param int $idLang Language ID
* @param int $idProduct Product ID
* @param bool $include Whether the attachments are included or excluded from the Product ID
*
* @return array|false|mysqli_result|PDOStatement|resource|null Database query result
*/
public static function getAttachments($idLang, $idProduct, $include = true)
{
return Db::getInstance()->executeS(
'
SELECT *
FROM ' . _DB_PREFIX_ . 'attachment a
LEFT JOIN ' . _DB_PREFIX_ . 'attachment_lang al
ON (a.id_attachment = al.id_attachment AND al.id_lang = ' . (int) $idLang . ')
WHERE a.id_attachment ' . ($include ? 'IN' : 'NOT IN') . ' (
SELECT pa.id_attachment
FROM ' . _DB_PREFIX_ . 'product_attachment pa
WHERE id_product = ' . (int) $idProduct . '
)'
);
}
/**
* Unassociate all products from the current object
*
* @param bool $updateAttachmentCache [default=true] If set to false attachment cache will not be updated
*
* @return bool Deletion result
*/
public function deleteAttachments(bool $updateAttachmentCache = true): bool
{
if (0 >= (int) $this->id) {
// Can not delete attachement without id
return false;
}
$res = Db::getInstance()->execute(
'DELETE FROM `' . _DB_PREFIX_ . 'product_attachment` ' .
'WHERE `id_attachment` = ' . (int) $this->id
);
if ($updateAttachmentCache === true) {
$productIds = Db::getInstance()->executeS(
'SELECT `id_product` FROM `' . _DB_PREFIX_ . 'product_attachment` ' .
'WHERE `id_attachment` = ' . (int) $this->id
);
foreach ($productIds as $productId) {
Product::updateCacheAttachment((int) $productId);
}
}
return $res;
}
/**
* Delete Product attachments for the given Product ID.
*
* @param int $idProduct Product ID
*
* @return bool
*/
public static function deleteProductAttachments($idProduct)
{
$res = Db::getInstance()->execute('
DELETE FROM ' . _DB_PREFIX_ . 'product_attachment
WHERE id_product = ' . (int) $idProduct);
Product::updateCacheAttachment((int) $idProduct);
return $res;
}
/**
* Associate $id_product to the current object.
*
* @param int $idProduct id of the product to associate
*
* @return bool true if success
*/
public function attachProduct($idProduct)
{
return static::associateProductAttachment((int) $idProduct, (int) $this->id);
}
/**
* @param int $productId
* @param int $attachmentId
*
* @return bool true if success
*/
public static function associateProductAttachment(int $productId, int $attachmentId): bool
{
$res = Db::getInstance()->execute('
INSERT INTO ' . _DB_PREFIX_ . 'product_attachment
(id_attachment, id_product) VALUES
(' . $attachmentId . ', ' . $productId . ')');
Product::updateCacheAttachment($productId);
return $res;
}
/**
* Associate an array of id_attachment $array to the product $id_product
* and remove eventual previous association.
*
* @param int $idProduct Product ID
* @param mixed $array Attachment IDs
*
* @return bool Whether the attachments have been successfully associated with the Product
*/
public static function attachToProduct($idProduct, $array)
{
$result1 = Attachment::deleteProductAttachments($idProduct);
if (is_array($array)) {
$ids = [];
foreach ($array as $idAttachment) {
if ((int) $idAttachment > 0) {
$ids[] = ['id_product' => (int) $idProduct, 'id_attachment' => (int) $idAttachment];
}
}
if (!empty($ids)) {
$result2 = Db::getInstance()->insert('product_attachment', $ids);
}
}
Product::updateCacheAttachment((int) $idProduct);
if (is_array($array)) {
return $result1 && (!isset($result2) || $result2);
}
return $result1;
}
/**
* Get Attachment IDs for the given Product within the given range of attachment IDs.
*
* @param int $idLang Language ID
* @param array $list List of attachment IDs in which to search
*
* @return array|bool List of attachment IDs found. False if nothing found.
*/
public static function getProductAttached($idLang, $list)
{
if (!is_array($list)) {
return false;
}
$idsAttachments = array_column($list, 'id_attachment');
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . 'product_attachment` pa ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (pa.`id_product` = pl.`id_product`' . Shop::addSqlRestrictionOnLang('pl') . ') ' .
'WHERE `id_attachment` IN (' . implode(',', array_map('intval', $idsAttachments)) . ') ' .
'AND pl.`id_lang` = ' . (int) $idLang;
$tmp = Db::getInstance()->executeS($sql);
$productAttachments = [];
foreach ($tmp as $t) {
$productAttachments[$t['id_attachment']][] = $t['name'];
}
return $productAttachments;
}
/**
* Get attachment products ids of current attachment for association.
*
* @return array<int, array{ id: string }> An array of product ids
*/
public function getWsProducts(): array
{
return Db::getInstance()->executeS(
'SELECT p.`id_product` AS id ' .
'FROM `' . _DB_PREFIX_ . 'product_attachment` pa ' .
'INNER JOIN `' . _DB_PREFIX_ . 'product` p ON (p.id_product = pa.id_product) ' .
'' . Shop::addSqlAssociation('product', 'p') . ' ' .
'WHERE pa.`id_attachment` = ' . (int) $this->id
);
}
/**
* Set products ids of current attachment for association.
*
* @param array<int, array{id: int|string }> $products Products ids
*
* @return bool
*/
public function setWsProducts(array $products): bool
{
$this->deleteAttachments(true);
foreach ($products as $product) {
Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'product_attachment` (`id_product`, `id_attachment`) VALUES (' . (int) $product['id'] . ', ' . (int) $this->id . ')');
Product::updateCacheAttachment((int) $product['id']);
}
return true;
}
}

391
classes/AttributeGroup.php Normal file
View File

@@ -0,0 +1,391 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class AttributeGroupCore.
*/
class AttributeGroupCore extends ObjectModel
{
/** @var string|string[] Name */
public $name;
/** @var bool Whether the attribute group is a color group */
public $is_color_group;
/** @var int Position */
public $position;
/** @var string Group type */
public $group_type;
/** @var string|string[] Public Name */
public $public_name;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'attribute_group',
'primary' => 'id_attribute_group',
'multilang' => true,
'fields' => [
'is_color_group' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'group_type' => ['type' => self::TYPE_STRING, 'required' => true, 'size' => 255, 'trans' => ['key' => 'Attribute type', 'domain' => 'Admin.Catalog.Feature']],
'position' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
/* Lang fields */
'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 128, 'trans' => ['key' => 'Name', 'domain' => 'Admin.Global']],
'public_name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 64, 'trans' => ['key' => 'Public name', 'domain' => 'Admin.Catalog.Feature']],
],
];
/** @var array Web service parameters */
protected $webserviceParameters = [
'objectsNodeName' => 'product_options',
'objectNodeName' => 'product_option',
'fields' => [],
'associations' => [
'product_option_values' => [
'resource' => 'product_option_value',
'fields' => [
'id' => [],
],
],
],
];
/**
* Adds current AttributeGroup as a new Object to the database.
*
* @param bool $autoDate Automatically set `date_upd` and `date_add` column
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Whether the AttributeGroup has been successfully added
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function add($autoDate = true, $nullValues = false)
{
$this->is_color_group = $this->group_type == 'color';
if ($this->position <= 0) {
$this->position = AttributeGroup::getHigherPosition() + 1;
}
$return = parent::add($autoDate, true);
Hook::exec('actionAttributeGroupSave', ['id_attribute_group' => $this->id]);
return $return;
}
/**
* Updates the current object in the database.
*
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Whether the AttributeGroup has been succesfully updated
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function update($nullValues = false)
{
$this->is_color_group = $this->group_type == 'color';
$return = parent::update($nullValues);
Hook::exec('actionAttributeGroupSave', ['id_attribute_group' => $this->id]);
return $return;
}
/**
* Clean dead combinations
* A combination is considered dead when its Attribute ID cannot be found.
*
* @return bool Whether the dead combinations have been successfully deleted
*/
public static function cleanDeadCombinations()
{
$attributeCombinations = Db::getInstance()->executeS('
SELECT pac.`id_attribute`, pa.`id_product_attribute`
FROM `' . _DB_PREFIX_ . 'product_attribute` pa
LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac
ON (pa.`id_product_attribute` = pac.`id_product_attribute`)
');
$toRemove = [];
foreach ($attributeCombinations as $attributeCombination) {
if ((int) $attributeCombination['id_attribute'] == 0) {
$toRemove[] = (int) $attributeCombination['id_product_attribute'];
}
}
$return = true;
if (!empty($toRemove)) {
foreach ($toRemove as $remove) {
$combination = new Combination($remove);
$return &= $combination->delete();
}
}
return $return;
}
/**
* Deletes current AttributeGroup from database.
*
* @return bool True if delete was successful
*
* @throws PrestaShopException
*/
public function delete()
{
if (!$this->hasMultishopEntries() || Shop::getContext() == Shop::CONTEXT_ALL) {
/* Select children in order to find linked combinations */
$attributeIds = Db::getInstance()->executeS(
'
SELECT `id_attribute`
FROM `' . _DB_PREFIX_ . 'attribute`
WHERE `id_attribute_group` = ' . (int) $this->id
);
if ($attributeIds === false) {
return false;
}
/* Removing attributes to the found combinations */
$toRemove = [];
foreach ($attributeIds as $attribute) {
$toRemove[] = (int) $attribute['id_attribute'];
}
if (!empty($toRemove) && Db::getInstance()->execute('
DELETE FROM `' . _DB_PREFIX_ . 'product_attribute_combination`
WHERE `id_attribute`
IN (' . implode(', ', $toRemove) . ')') === false) {
return false;
}
/* Remove combinations if they do not possess attributes anymore */
if (!AttributeGroup::cleanDeadCombinations()) {
return false;
}
/* Also delete related attributes */
if (count($toRemove)) {
if (!Db::getInstance()->execute('
DELETE FROM `' . _DB_PREFIX_ . 'attribute_lang`
WHERE `id_attribute` IN (' . implode(',', $toRemove) . ')')
|| !Db::getInstance()->execute('
DELETE FROM `' . _DB_PREFIX_ . 'attribute_shop`
WHERE `id_attribute` IN (' . implode(',', $toRemove) . ')')
|| !Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'attribute` WHERE `id_attribute_group` = ' . (int) $this->id)) {
return false;
}
}
$this->cleanPositions();
}
$return = parent::delete();
if ($return) {
Hook::exec('actionAttributeGroupDelete', ['id_attribute_group' => $this->id]);
}
return $return;
}
/**
* Get all attributes for a given language / group.
*
* @param int $idLang Language ID
* @param int $idAttributeGroup AttributeGroup ID
*
* @return array Attributes
*/
public static function getAttributes($idLang, $idAttributeGroup)
{
if (!Combination::isFeatureActive()) {
return [];
}
return Db::getInstance()->executeS('
SELECT *
FROM `' . _DB_PREFIX_ . 'attribute` a
' . Shop::addSqlAssociation('attribute', 'a') . '
LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al
ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) $idLang . ')
WHERE a.`id_attribute_group` = ' . (int) $idAttributeGroup . '
ORDER BY `position` ASC
');
}
/**
* Get all attributes groups for a given language.
*
* @param int $idLang Language id
*
* @return array Attributes groups
*/
public static function getAttributesGroups($idLang)
{
if (!Combination::isFeatureActive()) {
return [];
}
return Db::getInstance()->executeS('
SELECT DISTINCT agl.`name`, ag.*, agl.*
FROM `' . _DB_PREFIX_ . 'attribute_group` ag
' . Shop::addSqlAssociation('attribute_group', 'ag') . '
LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl
ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND `id_lang` = ' . (int) $idLang . ')
ORDER BY `name` ASC
');
}
/**
* Delete several objects from database.
*
* @param array $selection Array with AttributeGroup IDs
*
* @return bool Deletion result
*/
public function deleteSelection(array $selection)
{
/* Also delete Attributes */
foreach ($selection as $value) {
$obj = new AttributeGroup($value);
if (!$obj->delete()) {
return false;
}
}
return true;
}
/**
* Set the values of the current AttributeGroup for the webservice.
*
* @param array $values
*
* @return bool Whether the update was successful
*/
public function setWsProductOptionValues($values)
{
$ids = [];
foreach ($values as $value) {
$ids[] = (int) $value['id'];
}
if (!empty($ids)) {
Db::getInstance()->execute(
'
DELETE FROM `' . _DB_PREFIX_ . 'attribute`
WHERE `id_attribute_group` = ' . (int) $this->id . '
AND `id_attribute` NOT IN (' . implode(',', $ids) . ')'
);
}
$ok = true;
foreach ($values as $value) {
$result = Db::getInstance()->execute(
'
UPDATE `' . _DB_PREFIX_ . 'attribute`
SET `id_attribute_group` = ' . (int) $this->id . '
WHERE `id_attribute` = ' . (int) $value['id']
);
if ($result === false) {
$ok = false;
}
}
return $ok;
}
/**
* Get values of current AttributeGroup instance for the webservice.
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public function getWsProductOptionValues()
{
$result = Db::getInstance()->executeS(
'
SELECT a.id_attribute AS id
FROM `' . _DB_PREFIX_ . 'attribute` a
' . Shop::addSqlAssociation('attribute', 'a') . '
WHERE a.id_attribute_group = ' . (int) $this->id
);
return $result;
}
/**
* Move a group attribute.
*
* @param bool $direction Up (1) or Down (0)
* @param int|null $position
*
* @return bool Update result
*/
public function updatePosition($direction, $position)
{
if (!$res = Db::getInstance()->executeS(
'
SELECT ag.`position`, ag.`id_attribute_group`
FROM `' . _DB_PREFIX_ . 'attribute_group` ag
WHERE ag.`id_attribute_group` = ' . (int) Tools::getValue('id_attribute_group', 1) . '
ORDER BY ag.`position` ASC'
)) {
return false;
}
foreach ($res as $groupAttribute) {
if ((int) $groupAttribute['id_attribute_group'] == (int) $this->id) {
$movedGroupAttribute = $groupAttribute;
}
}
if (!isset($movedGroupAttribute) || !isset($position)) {
return false;
}
// < and > statements rather than BETWEEN operator
// since BETWEEN is treated differently according to databases
return Db::getInstance()->execute(
'
UPDATE `' . _DB_PREFIX_ . 'attribute_group`
SET `position`= `position` ' . ($direction ? '- 1' : '+ 1') . '
WHERE `position`
' . ($direction
? '> ' . (int) $movedGroupAttribute['position'] . ' AND `position` <= ' . (int) $position
: '< ' . (int) $movedGroupAttribute['position'] . ' AND `position` >= ' . (int) $position)
) && Db::getInstance()->execute('
UPDATE `' . _DB_PREFIX_ . 'attribute_group`
SET `position` = ' . (int) $position . '
WHERE `id_attribute_group`=' . (int) $movedGroupAttribute['id_attribute_group']);
}
/**
* Reorder group attribute position
* Call it after deleting a group attribute.
*
* @return bool $return
*/
public static function cleanPositions()
{
$return = true;
Db::getInstance()->execute('SET @i = -1', false);
$return = Db::getInstance()->execute(
'
UPDATE `' . _DB_PREFIX_ . 'attribute_group`
SET `position` = @i:=@i+1
ORDER BY `position`'
);
return $return;
}
/**
* Get the highest AttributeGroup position.
*
* @return int $position Position
*/
public static function getHigherPosition()
{
$sql = 'SELECT MAX(`position`)
FROM `' . _DB_PREFIX_ . 'attribute_group`';
$position = Db::getInstance()->getValue($sql);
return (is_numeric($position)) ? $position : -1;
}
}

327
classes/CMS.php Normal file
View File

@@ -0,0 +1,327 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class CMSCore.
*/
class CMSCore extends ObjectModel
{
/** @var int|null */
public $id;
public $id_cms;
public $head_seo_title;
public $meta_title;
public $meta_description;
public $content;
public $link_rewrite;
public $id_cms_category;
public $position;
public $indexation;
public $active;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'cms',
'primary' => 'id_cms',
'multilang' => true,
'multilang_shop' => true,
'fields' => [
'id_cms_category' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
'position' => ['type' => self::TYPE_INT],
'indexation' => ['type' => self::TYPE_BOOL],
'active' => ['type' => self::TYPE_BOOL],
/* Lang fields */
'meta_description' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 512],
'meta_title' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 255],
'head_seo_title' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
'link_rewrite' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isLinkRewrite', 'required' => true, 'size' => 128],
'content' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml', 'size' => 1073741823],
],
];
protected $webserviceParameters = [
'objectNodeName' => 'content',
'objectsNodeName' => 'content_management_system',
];
/**
* Adds current CMS as a new Object to the database.
*
* @param bool $autoDate Automatically set `date_upd` and `date_add` columns
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Indicates whether the CMS has been successfully added
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function add($autoDate = true, $nullValues = false)
{
$this->position = CMS::getLastPosition((int) $this->id_cms_category);
return parent::add($autoDate, true);
}
/**
* Updates the current CMS in the database.
*
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Indicates whether the CMS has been successfully updated
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function update($nullValues = false)
{
if (parent::update($nullValues)) {
return $this->cleanPositions($this->id_cms_category);
}
return false;
}
/**
* Deletes current CMS from the database.
*
* @return bool True if delete was successful
*
* @throws PrestaShopException
*/
public function delete()
{
if (parent::delete()) {
return $this->cleanPositions($this->id_cms_category);
}
return false;
}
/**
* Get links.
*
* @param int $idLang Language ID
* @param array|null $selection
* @param bool $active
* @param Link|null $link
*
* @return array
*/
public static function getLinks($idLang, $selection = null, $active = true, ?Link $link = null)
{
if (!$link) {
$link = Context::getContext()->link;
}
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT c.id_cms, cl.link_rewrite, cl.meta_title
FROM ' . _DB_PREFIX_ . 'cms c
LEFT JOIN ' . _DB_PREFIX_ . 'cms_lang cl ON (c.id_cms = cl.id_cms AND cl.id_lang = ' . (int) $idLang . ' AND cl.id_shop = ' . (int) Context::getContext()->shop->id . ')
' . Shop::addSqlAssociation('cms', 'c') . '
WHERE 1
' . (($selection !== null) ? ' AND c.id_cms IN (' . implode(',', array_map('intval', $selection)) . ')' : '') .
($active ? ' AND c.`active` = 1 ' : '') .
'GROUP BY c.id_cms
ORDER BY c.`position`');
$links = [];
if ($result) {
foreach ($result as $row) {
$row['link'] = $link->getCMSLink((int) $row['id_cms'], $row['link_rewrite']);
$links[] = $row;
}
}
return $links;
}
/**
* @param int|null $idLang
* @param bool $idBlock
* @param bool $active
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public static function listCms($idLang = null, $idBlock = false, $active = true)
{
if (empty($idLang)) {
$idLang = (int) Configuration::get('PS_LANG_DEFAULT');
}
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT c.id_cms, l.meta_title
FROM ' . _DB_PREFIX_ . 'cms c
JOIN ' . _DB_PREFIX_ . 'cms_lang l ON (c.id_cms = l.id_cms)
' . Shop::addSqlAssociation('cms', 'c') . '
' . (($idBlock) ? 'JOIN ' . _DB_PREFIX_ . 'block_cms b ON (c.id_cms = b.id_cms)' : '') . '
WHERE l.id_lang = ' . (int) $idLang . (($idBlock) ? ' AND b.id_block = ' . (int) $idBlock : '') . ($active ? ' AND c.`active` = 1 ' : '') . '
GROUP BY c.id_cms
ORDER BY c.`position`');
}
/**
* @param int|null $way
* @param int|null $position
*
* @return bool
*/
public function updatePosition($way, $position)
{
if (!$res = Db::getInstance()->executeS(
'
SELECT cp.`id_cms`, cp.`position`, cp.`id_cms_category`
FROM `' . _DB_PREFIX_ . 'cms` cp
WHERE cp.`id_cms_category` = ' . (int) $this->id_cms_category . '
ORDER BY cp.`position` ASC'
)) {
return false;
}
foreach ($res as $cms) {
if ((int) $cms['id_cms'] == (int) $this->id) {
$movedCms = $cms;
}
}
if (!isset($movedCms) || !isset($position)) {
return false;
}
// < and > statements rather than BETWEEN operator
// since BETWEEN is treated differently according to databases
return Db::getInstance()->execute('
UPDATE `' . _DB_PREFIX_ . 'cms`
SET `position`= `position` ' . ($way ? '- 1' : '+ 1') . '
WHERE `position`
' . ($way
? '> ' . (int) $movedCms['position'] . ' AND `position` <= ' . (int) $position
: '< ' . (int) $movedCms['position'] . ' AND `position` >= ' . (int) $position) . '
AND `id_cms_category`=' . (int) $movedCms['id_cms_category'])
&& Db::getInstance()->execute('
UPDATE `' . _DB_PREFIX_ . 'cms`
SET `position` = ' . (int) $position . '
WHERE `id_cms` = ' . (int) $movedCms['id_cms'] . '
AND `id_cms_category`=' . (int) $movedCms['id_cms_category']);
}
/**
* @param int $idCategory
*
* @return bool
*/
public static function cleanPositions($idCategory)
{
$sql = '
SELECT `id_cms`
FROM `' . _DB_PREFIX_ . 'cms`
WHERE `id_cms_category` = ' . (int) $idCategory . '
ORDER BY `position`';
$result = Db::getInstance()->executeS($sql);
for ($i = 0, $total = count($result); $i < $total; ++$i) {
$sql = 'UPDATE `' . _DB_PREFIX_ . 'cms`
SET `position` = ' . (int) $i . '
WHERE `id_cms_category` = ' . (int) $idCategory . '
AND `id_cms` = ' . (int) $result[$i]['id_cms'];
Db::getInstance()->execute($sql);
}
return true;
}
/**
* Returns the next position to use for a new CMS page.
* CMS page positions start with 0.
* Returns position of the last CMS page within that category + 1,
* 0 if no CMS pages exist in the category.
*
* @param int $idCmsCategory ID of the CMS category the page will belong to
*
* @return int Position to use
*/
public static function getLastPosition($idCmsCategory)
{
return (int) Db::getInstance()->getValue('SELECT MAX(position) + 1
FROM `' . _DB_PREFIX_ . 'cms`
WHERE `id_cms_category` = ' . (int) $idCmsCategory);
}
/**
* @param int|null $idLang
* @param int|null $idCmsCategory
* @param bool $active
* @param int|null $idShop
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public static function getCMSPages($idLang = null, $idCmsCategory = null, $active = true, $idShop = null)
{
$sql = new DbQuery();
$sql->select('*');
$sql->from('cms', 'c');
if ($idLang) {
if ($idShop) {
$sql->innerJoin('cms_lang', 'l', 'c.id_cms = l.id_cms AND l.id_lang = ' . (int) $idLang . ' AND l.id_shop = ' . (int) $idShop);
} else {
$sql->innerJoin('cms_lang', 'l', 'c.id_cms = l.id_cms AND l.id_lang = ' . (int) $idLang);
}
}
if ($idShop) {
$sql->innerJoin('cms_shop', 'cs', 'c.id_cms = cs.id_cms AND cs.id_shop = ' . (int) $idShop);
}
if ($active) {
$sql->where('c.active = 1');
}
if ($idCmsCategory) {
$sql->where('c.id_cms_category = ' . (int) $idCmsCategory);
}
$sql->orderBy('position');
return Db::getInstance()->executeS($sql);
}
/**
* @param int $idCms
* @param int|null $idLang
* @param int|null $idShop
*
* @return array|bool|object|null
*/
public static function getCMSContent($idCms, $idLang = null, $idShop = null)
{
if (null === $idLang) {
$idLang = (int) Configuration::get('PS_LANG_DEFAULT');
}
if (null === $idShop) {
$idShop = (int) Configuration::get('PS_SHOP_DEFAULT');
}
$sql = '
SELECT `content`
FROM `' . _DB_PREFIX_ . 'cms_lang`
WHERE `id_cms` = ' . (int) $idCms . ' AND `id_lang` = ' . (int) $idLang . ' AND `id_shop` = ' . (int) $idShop;
return Db::getInstance()->getRow($sql);
}
/**
* Method required for new PrestaShop Core.
*
* @return string
*/
public static function getRepositoryClassName()
{
return '\\PrestaShop\\PrestaShop\\Core\\CMS\\CMSRepository';
}
}

657
classes/CMSCategory.php Normal file
View File

@@ -0,0 +1,657 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShopBundle\Form\Admin\Type\FormattedTextareaType;
class CMSCategoryCore extends ObjectModel
{
public $id;
/** @var int CMSCategory ID */
public $id_cms_category;
/** @var string|array<int, string> Name */
public $name;
/** @var bool Status for display */
public $active = true;
/** @var string|array<int, string> Description */
public $description;
/** @var int Parent CMSCategory ID */
public $id_parent;
/** @var int category position */
public $position;
/** @var int Parents number */
public $level_depth;
/** @var string|array<int, string> string used in rewrited URL */
public $link_rewrite;
/** @var string|array<int, string> Meta title */
public $meta_title;
/** @var string|array<int, string> Meta description */
public $meta_description;
/** @var string Object creation date */
public $date_add;
/** @var string Object last modification date */
public $date_upd;
protected static $_links = [];
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'cms_category',
'primary' => 'id_cms_category',
'multilang' => true,
'multilang_shop' => true,
'fields' => [
'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'required' => true],
'id_parent' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'required' => true],
'position' => ['type' => self::TYPE_INT],
'level_depth' => ['type' => self::TYPE_INT],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
/* Lang fields */
'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isCatalogName', 'required' => true, 'size' => 128],
'link_rewrite' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isLinkRewrite', 'required' => true, 'size' => 128],
'description' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isCleanHtml', 'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4],
'meta_title' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
'meta_description' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 512],
],
];
public function add($autodate = true, $null_values = false)
{
$this->position = CMSCategory::getLastPosition((int) $this->id_parent);
$this->level_depth = $this->calcLevelDepth();
foreach ($this->name as $k => $value) {
if (preg_match('/^[1-9]\./', $value)) {
$this->name[$k] = '0' . $value;
}
}
$ret = parent::add($autodate, $null_values);
$this->cleanPositions($this->id_parent);
return $ret;
}
public function update($null_values = false)
{
$this->level_depth = $this->calcLevelDepth();
foreach ($this->name as $k => $value) {
if (preg_match('/^[1-9]\./', $value)) {
$this->name[$k] = '0' . $value;
}
}
return parent::update($null_values);
}
/**
* Recursive scan of subcategories.
*
* @param int $max_depth Maximum depth of the tree (i.e. 2 => 3 levels depth)
* @param int $currentDepth specify the current depth in the tree (don't use it, only for rucursivity!)
* @param int|null $id_lang Specify the id of the language used
* @param array|null $excluded_ids_array specify a list of ids to exclude of results
* @param Link|null $link
*
* @return array Subcategories lite tree
*/
public function recurseLiteCategTree($max_depth = 3, $currentDepth = 0, $id_lang = null, $excluded_ids_array = null, ?Link $link = null)
{
if (!$link) {
$link = Context::getContext()->link;
}
if (null === $id_lang) {
$id_lang = Context::getContext()->language->id;
}
// recursivity for subcategories
$children = [];
$subcats = $this->getSubCategories($id_lang, true);
if (($max_depth == 0 || $currentDepth < $max_depth) && count($subcats)) {
foreach ($subcats as $subcat) {
if (!$subcat['id_cms_category']) {
break;
} elseif (!is_array($excluded_ids_array) || !in_array($subcat['id_cms_category'], $excluded_ids_array)) {
$categ = new CMSCategory($subcat['id_cms_category'], $id_lang);
$categ->name = CMSCategory::hideCMSCategoryPosition($categ->name);
$children[] = $categ->recurseLiteCategTree($max_depth, $currentDepth + 1, $id_lang, $excluded_ids_array);
}
}
}
return [
'id' => $this->id_cms_category,
'link' => $link->getCMSCategoryLink($this->id, $this->link_rewrite),
'name' => $this->name,
'desc' => $this->description,
'children' => $children,
];
}
public static function getRecurseCategory($id_lang = null, $current = 1, $active = 1, $links = 0, ?Link $link = null)
{
if (!$link) {
$link = Context::getContext()->link;
}
if (null === $id_lang) {
$id_lang = Context::getContext()->language->id;
}
$sql = 'SELECT c.`id_cms_category`, c.`id_parent`, c.`level_depth`, cl.`name`, cl.`link_rewrite`
FROM `' . _DB_PREFIX_ . 'cms_category` c
JOIN `' . _DB_PREFIX_ . 'cms_category_lang` cl ON c.`id_cms_category` = cl.`id_cms_category`
WHERE c.`id_cms_category` = ' . (int) $current . '
AND `id_lang` = ' . (int) $id_lang;
/** @var array $category */
$category = Db::getInstance()->getRow($sql);
$sql = 'SELECT c.`id_cms_category`
FROM `' . _DB_PREFIX_ . 'cms_category` c
' . Shop::addSqlAssociation('cms_category', 'c') . '
WHERE c.`id_parent` = ' . (int) $current .
($active ? ' AND c.`active` = 1' : '') .
' ORDER BY c.`position`';
$result = Db::getInstance()->executeS($sql);
foreach ($result as $row) {
$category['children'][] = CMSCategory::getRecurseCategory($id_lang, $row['id_cms_category'], $active, $links);
}
$sql = 'SELECT c.`id_cms`, cl.`meta_title`, cl.`link_rewrite`
FROM `' . _DB_PREFIX_ . 'cms` c
' . Shop::addSqlAssociation('cms', 'c') . '
JOIN `' . _DB_PREFIX_ . 'cms_lang` cl ON c.`id_cms` = cl.`id_cms`
WHERE `id_cms_category` = ' . (int) $current . ($active ? ' AND c.`active` = 1' : '') . '
AND cl.`id_shop` = ' . (int) Context::getContext()->shop->id . '
AND cl.`id_lang` = ' . (int) $id_lang . '
GROUP BY c.id_cms
ORDER BY c.`position`';
$category['cms'] = Db::getInstance()->executeS($sql);
if ($links == 1) {
$category['link'] = $link->getCMSCategoryLink($current, $category['link_rewrite']);
foreach ($category['cms'] as $key => $cms) {
$category['cms'][$key]['link'] = $link->getCMSLink($cms['id_cms'], $cms['link_rewrite']);
}
}
return $category;
}
public static function recurseCMSCategory($categories, $current, $id_cms_category = 1, $id_selected = 1, $is_html = 0)
{
$html = '<option value="' . $id_cms_category . '"' . (($id_selected == $id_cms_category) ? ' selected="selected"' : '') . '>'
. str_repeat('&nbsp;', $current['infos']['level_depth'] * 5)
. CMSCategory::hideCMSCategoryPosition(stripslashes($current['infos']['name'])) . '</option>';
if ($is_html == 0) {
echo $html;
}
if (isset($categories[$id_cms_category])) {
foreach (array_keys($categories[$id_cms_category]) as $key) {
$html .= CMSCategory::recurseCMSCategory($categories, $categories[$id_cms_category][$key], $key, $id_selected, $is_html);
}
}
return $html;
}
/**
* Recursively add specified CMSCategory childs to $toDelete array.
*
* @param array $to_delete Array reference where categories ID will be saved
* @param array|int $id_cms_category Parent CMSCategory ID
*/
protected function recursiveDelete(array &$to_delete, $id_cms_category)
{
if (!$id_cms_category) {
throw new PrestaShopException('Parameter "id_cms_category" is invalid.');
}
$result = Db::getInstance()->executeS('
SELECT `id_cms_category`
FROM `' . _DB_PREFIX_ . 'cms_category`
WHERE `id_parent` = ' . (int) $id_cms_category);
foreach ($result as $row) {
$to_delete[] = (int) $row['id_cms_category'];
$this->recursiveDelete($to_delete, (int) $row['id_cms_category']);
}
}
/**
* Directly call the parent of delete, in order to avoid recursion.
*
* @return bool Deletion result
*/
private function deleteLite()
{
return parent::delete();
}
public function delete()
{
if ((int) $this->id === 1) {
return false;
}
$this->clearCache();
/** @var array<CMSCategory> $cmsCategories */
$cmsCategories = $this->getAllChildren();
$cmsCategories[] = $this;
foreach ($cmsCategories as $cmsCategory) {
$cmsCategory->deleteCMS();
$cmsCategory->deleteLite();
CMSCategory::cleanPositions($cmsCategory->id_parent);
}
return true;
}
/**
* Delete pages which are in CMSCategories to delete.
*
* @return bool Deletion result
*/
private function deleteCMS()
{
$result = true;
$cms = new PrestaShopCollection('CMS');
$cms->where('id_cms_category', '=', $this->id);
foreach ($cms as $c) {
$result &= $c->delete();
}
return $result;
}
/**
* Delete several categories from database.
*
* return boolean Deletion result
*/
public function deleteSelection(array $categories)
{
$return = true;
foreach ($categories as $id_category_cms) {
$category_cms = new CMSCategory($id_category_cms);
$return = $return && $category_cms->delete();
}
return $return;
}
/**
* Get the number of parent categories.
*
* @return int Level depth
*/
public function calcLevelDepth()
{
$parentCMSCategory = new CMSCategory($this->id_parent);
return $parentCMSCategory->level_depth + 1;
}
/**
* Return available categories.
*
* @param int $id_lang Language ID
* @param bool $active return only active categories
*
* @return array Categories
*/
public static function getCategories($id_lang, bool $active = true, $order = true)
{
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT *
FROM `' . _DB_PREFIX_ . 'cms_category` c
LEFT JOIN `' . _DB_PREFIX_ . 'cms_category_lang` cl ON c.`id_cms_category` = cl.`id_cms_category`
WHERE `id_lang` = ' . (int) $id_lang . '
' . ($active ? 'AND `active` = 1' : '') . '
ORDER BY `name` ASC');
if (!$order) {
return $result;
}
$categories = [];
foreach ($result as $row) {
$categories[$row['id_parent']][$row['id_cms_category']]['infos'] = $row;
}
return $categories;
}
public static function getSimpleCategories($id_lang)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT c.`id_cms_category`, cl.`name`
FROM `' . _DB_PREFIX_ . 'cms_category` c
LEFT JOIN `' . _DB_PREFIX_ . 'cms_category_lang` cl ON (c.`id_cms_category` = cl.`id_cms_category`)
WHERE cl.`id_lang` = ' . (int) $id_lang . '
ORDER BY cl.`name`');
}
/**
* Return current CMSCategory childs.
*
* @param int $id_lang Language ID
* @param bool $active return only active categories
*
* @return array Categories
*/
public function getSubCategories(int $id_lang, bool $active = true)
{
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT c.*, cl.id_lang, cl.name, cl.description, cl.link_rewrite, cl.meta_title, cl.meta_description
FROM `' . _DB_PREFIX_ . 'cms_category` c
LEFT JOIN `' . _DB_PREFIX_ . 'cms_category_lang` cl ON (c.`id_cms_category` = cl.`id_cms_category` AND `id_lang` = ' . (int) $id_lang . ')
WHERE `id_parent` = ' . (int) $this->id . '
' . ($active ? 'AND `active` = 1' : '') . '
GROUP BY c.`id_cms_category`
ORDER BY `position` ASC');
// Modify SQL result
foreach ($result as &$row) {
$row['name'] = CMSCategory::hideCMSCategoryPosition($row['name']);
}
return $result;
}
/**
* Hide CMSCategory prefix used for position.
*
* @param string $name CMSCategory name
*
* @return string Name without position
*/
public static function hideCMSCategoryPosition($name)
{
return preg_replace('/^[0-9]+\./', '', $name);
}
/**
* Return main categories.
*
* @param int $id_lang Language ID
* @param bool $active return only active categories
*
* @return array categories
*/
public static function getHomeCategories($id_lang, $active = true)
{
return CMSCategory::getChildren(1, $id_lang, $active);
}
public static function getChildren($id_parent, $id_lang, bool $active = true)
{
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT c.`id_cms_category`, cl.`name`, cl.`link_rewrite`
FROM `' . _DB_PREFIX_ . 'cms_category` c
LEFT JOIN `' . _DB_PREFIX_ . 'cms_category_lang` cl ON c.`id_cms_category` = cl.`id_cms_category`
WHERE `id_lang` = ' . (int) $id_lang . '
AND c.`id_parent` = ' . (int) $id_parent . '
' . ($active ? 'AND `active` = 1' : '') . '
ORDER BY `name` ASC');
// Modify SQL result
$results_array = [];
foreach ($result as $row) {
$row['name'] = CMSCategory::hideCMSCategoryPosition($row['name']);
$results_array[] = $row;
}
return $results_array;
}
/**
* Return an array of all children of the current CMSCategory.
*
* @return PrestaShopCollection|array Collection of CMSCategory
*/
private function getAllChildren()
{
// Get children
$toDelete = [(int) $this->id];
$this->recursiveDelete($toDelete, (int) $this->id);
$toDelete = array_unique($toDelete);
// remove id of current CMSCategory because we want only ids of children
unset($toDelete[0]);
if (count($toDelete)) {
$children = new PrestaShopCollection('CMSCategory');
$children->where('id_cms_category', 'in', $toDelete);
return $children;
}
return $toDelete;
}
/**
* Check if CMSCategory can be moved in another one.
*
* @param int $id_parent Parent candidate
*
* @return bool Parent validity
*/
public static function checkBeforeMove($id_cms_category, $id_parent)
{
if ($id_cms_category == $id_parent) {
return false;
}
if ($id_parent == 1) {
return true;
}
$i = (int) $id_parent;
while (42) {
$result = Db::getInstance()->getRow('SELECT `id_parent` FROM `' . _DB_PREFIX_ . 'cms_category` WHERE `id_cms_category` = ' . (int) $i);
if (!isset($result['id_parent'])) {
return false;
}
if ($result['id_parent'] == $id_cms_category) {
return false;
}
if ($result['id_parent'] == 1) {
return true;
}
$i = $result['id_parent'];
}
}
public static function getLinkRewrite($id_cms_category, $id_lang)
{
if (!Validate::isUnsignedId($id_cms_category) || !Validate::isUnsignedId($id_lang)) {
return false;
}
if (isset(self::$_links[$id_cms_category . '-' . $id_lang])) {
return self::$_links[$id_cms_category . '-' . $id_lang];
}
$result = Db::getInstance()->getRow('
SELECT cl.`link_rewrite`
FROM `' . _DB_PREFIX_ . 'cms_category` c
LEFT JOIN `' . _DB_PREFIX_ . 'cms_category_lang` cl ON c.`id_cms_category` = cl.`id_cms_category`
WHERE `id_lang` = ' . (int) $id_lang . '
AND c.`id_cms_category` = ' . (int) $id_cms_category);
self::$_links[$id_cms_category . '-' . $id_lang] = $result['link_rewrite'];
return $result['link_rewrite'];
}
public function getLink(?Link $link = null)
{
if (!$link) {
$link = Context::getContext()->link;
}
return $link->getCMSCategoryLink($this->id, $this->link_rewrite);
}
public function getName($id_lang = null)
{
$context = Context::getContext();
if (!$id_lang) {
if (isset($this->name[$context->language->id])) {
$id_lang = $context->language->id;
} else {
$id_lang = (int) Configuration::get('PS_LANG_DEFAULT');
}
}
return isset($this->name[$id_lang]) ? $this->name[$id_lang] : '';
}
/**
* Light back office search for categories.
*
* @param int $id_lang Language ID
* @param string $query Searched string
* @param bool $unrestricted allows search without lang and includes first CMSCategory and exact match
*
* @return array Corresponding categories
*/
public static function searchByName($id_lang, $query, $unrestricted = false)
{
if ($unrestricted === true) {
return Db::getInstance()->getRow('
SELECT c.*, cl.*
FROM `' . _DB_PREFIX_ . 'cms_category` c
LEFT JOIN `' . _DB_PREFIX_ . 'cms_category_lang` cl ON (c.`id_cms_category` = cl.`id_cms_category`)
WHERE `name` = \'' . pSQL($query) . '\'');
} else {
return Db::getInstance()->executeS('
SELECT c.*, cl.*
FROM `' . _DB_PREFIX_ . 'cms_category` c
LEFT JOIN `' . _DB_PREFIX_ . 'cms_category_lang` cl ON (c.`id_cms_category` = cl.`id_cms_category` AND `id_lang` = ' . (int) $id_lang . ')
WHERE `name` LIKE \'%' . pSQL($query) . '%\' AND c.`id_cms_category` != 1');
}
}
/**
* Get Each parent CMSCategory of this CMSCategory until the root CMSCategory.
*
* @param int $id_lang Language ID
*
* @return array Corresponding categories
*/
public function getParentsCategories($id_lang = null)
{
if (null === $id_lang) {
$id_lang = Context::getContext()->language->id;
}
$categories = null;
$id_current = $this->id;
while (true) {
$query = '
SELECT c.*, cl.*
FROM `' . _DB_PREFIX_ . 'cms_category` c
LEFT JOIN `' . _DB_PREFIX_ . 'cms_category_lang` cl ON (c.`id_cms_category` = cl.`id_cms_category` AND `id_lang` = ' . (int) $id_lang . ')
WHERE c.`id_cms_category` = ' . (int) $id_current . ' AND c.`id_parent` != 0
';
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query);
$categories[] = $result[0];
if (!$result || $result[0]['id_parent'] == 1) {
return $categories;
}
$id_current = $result[0]['id_parent'];
}
}
public function updatePosition($way, $position)
{
if (!$res = Db::getInstance()->executeS(
'
SELECT cp.`id_cms_category`, cp.`position`, cp.`id_parent`
FROM `' . _DB_PREFIX_ . 'cms_category` cp
WHERE cp.`id_parent` = ' . (int) $this->id_parent . '
ORDER BY cp.`position` ASC'
)) {
return false;
}
foreach ($res as $category) {
if ((int) $category['id_cms_category'] == (int) $this->id) {
$moved_category = $category;
}
}
if (!isset($moved_category) || !isset($position)) {
return false;
}
// < and > statements rather than BETWEEN operator
// since BETWEEN is treated differently according to databases
return Db::getInstance()->execute('
UPDATE `' . _DB_PREFIX_ . 'cms_category`
SET `position`= `position` ' . ($way ? '- 1' : '+ 1') . '
WHERE `position`
' . ($way
? '> ' . (int) $moved_category['position'] . ' AND `position` <= ' . (int) $position
: '< ' . (int) $moved_category['position'] . ' AND `position` >= ' . (int) $position) . '
AND `id_parent`=' . (int) $moved_category['id_parent'])
&& Db::getInstance()->execute('
UPDATE `' . _DB_PREFIX_ . 'cms_category`
SET `position` = ' . (int) $position . '
WHERE `id_parent` = ' . (int) $moved_category['id_parent'] . '
AND `id_cms_category`=' . (int) $moved_category['id_cms_category']);
}
public static function cleanPositions($id_category_parent)
{
$result = Db::getInstance()->executeS('
SELECT `id_cms_category`
FROM `' . _DB_PREFIX_ . 'cms_category`
WHERE `id_parent` = ' . (int) $id_category_parent . '
ORDER BY `position`');
$sizeof = count($result);
for ($i = 0; $i < $sizeof; ++$i) {
$sql = '
UPDATE `' . _DB_PREFIX_ . 'cms_category`
SET `position` = ' . (int) $i . '
WHERE `id_parent` = ' . (int) $id_category_parent . '
AND `id_cms_category` = ' . (int) $result[$i]['id_cms_category'];
Db::getInstance()->execute($sql);
}
return true;
}
/**
* Returns the next position to use for a new CMS category.
* CMS category positions start with 0.
* Returns position of the last CMS category within that category + 1,
* 0 if there are no CMS categories.
*
* @param int $idParentCmsCategory ID of the parent CMS category
*
* @return int Position to use
*/
public static function getLastPosition($idParentCmsCategory)
{
return (int) Db::getInstance()->getValue('SELECT MAX(position) + 1
FROM `' . _DB_PREFIX_ . 'cms_category`
WHERE `id_parent` = ' . (int) $idParentCmsCategory);
}
}

36
classes/CMSRole.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class CMSRoleCore.
*/
class CMSRoleCore extends ObjectModel
{
/** @var string name */
public $name;
/** @var int id_cms */
public $id_cms;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'cms_role',
'primary' => 'id_cms_role',
'fields' => [
'name' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 50],
'id_cms' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
],
];
/**
* @return string
*/
public static function getRepositoryClassName()
{
return '\\PrestaShop\\PrestaShop\\Core\\CMS\\CMSRoleRepository';
}
}

91
classes/CSV.php Normal file
View File

@@ -0,0 +1,91 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Simple class to output CSV data
* Uses CollectionCore.
*/
class CSVCore
{
public $filename;
public $collection;
public $delimiter;
/**
* Loads objects, filename and optionally a delimiter.
*
* @param array|Iterator $collection Collection of objects / arrays (of non-objects)
* @param string $filename used later to save the file
* @param string $delimiter delimiter used
*/
public function __construct($collection, $filename, $delimiter = ';')
{
$this->filename = $filename;
$this->delimiter = $delimiter;
$this->collection = $collection;
}
/**
* Main function
* Adds headers
* Outputs.
*/
public function export()
{
$this->headers();
$headerLine = false;
foreach ($this->collection as $object) {
$vars = get_object_vars($object);
if (!$headerLine) {
$this->output(array_keys($vars));
$headerLine = true;
}
// outputs values
$this->output($vars);
unset($vars);
}
}
/**
* Wraps data and echoes
* Uses defined delimiter.
*
* @param array $data
*/
public function output($data)
{
$wrappedData = array_map(['CSVCore', 'wrap'], $data);
echo sprintf("%s\n", implode($this->delimiter, $wrappedData));
}
/**
* Escapes data.
*
* @param string $data
*
* @return string $data
*/
public static function wrap($data)
{
$data = str_replace(['"', ';'], '', $data);
return sprintf('"%s"', $data);
}
/**
* Adds headers.
*/
public function headers()
{
header('Content-type: text/csv');
header('Content-Type: application/force-download; charset=UTF-8');
header('Cache-Control: no-store, no-cache');
header('Content-disposition: attachment; filename="' . $this->filename . '.csv"');
}
}

1706
classes/Carrier.php Normal file

File diff suppressed because it is too large Load Diff

5241
classes/Cart.php Normal file

File diff suppressed because it is too large Load Diff

2219
classes/CartRule.php Normal file

File diff suppressed because it is too large Load Diff

2459
classes/Category.php Normal file

File diff suppressed because it is too large Load Diff

122
classes/Chart.php Normal file
View File

@@ -0,0 +1,122 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
class ChartCore
{
/** @var int */
protected static $poolId = 0;
protected $width = 600;
protected $height = 300;
/* Time mode */
protected $timeMode = false;
protected $from;
protected $to;
protected $format;
protected $granularity;
protected $curves = [];
/** @prototype void public static function init(void) */
public static function init()
{
if (!self::$poolId) {
++self::$poolId;
return true;
}
}
/** @prototype void public function __construct() */
public function __construct()
{
++self::$poolId;
}
/** @prototype void public function setSize(int $width, int $height) */
public function setSize($width, $height)
{
$this->width = (int) $width;
$this->height = (int) $height;
}
/** @prototype void public function setTimeMode($from, $to, $granularity) */
public function setTimeMode($from, $to, $granularity)
{
$this->granularity = $granularity;
if (Validate::isDate($from)) {
$from = strtotime($from);
}
$this->from = $from;
if (Validate::isDate($to)) {
$to = strtotime($to);
}
$this->to = $to;
if ($granularity == 'd') {
$this->format = '%d/%m/%y';
}
if ($granularity == 'w') {
$this->format = '%d/%m/%y';
}
if ($granularity == 'm') {
$this->format = '%m/%y';
}
if ($granularity == 'y') {
$this->format = '%y';
}
$this->timeMode = true;
}
public function getCurve($i)
{
if (!array_key_exists($i, $this->curves)) {
$this->curves[$i] = new Curve();
}
return $this->curves[$i];
}
/** @prototype void public function display() */
public function display()
{
echo $this->fetch();
}
public function fetch()
{
if ($this->timeMode) {
$options = 'xaxis:{mode:"time",timeformat:\'' . addslashes($this->format) . '\',min:' . $this->from . '000,max:' . $this->to . '000}';
if ($this->granularity == 'd') {
foreach ($this->curves as $curve) {
/* @var Curve $curve */
for ($i = $this->from; $i <= $this->to; $i = strtotime('+1 day', $i)) {
if (!$curve->getPoint($i)) {
$curve->setPoint($i, 0);
}
}
}
}
}
$jsCurves = [];
foreach ($this->curves as $curve) {
$jsCurves[] = $curve->getValues($this->timeMode);
}
if (count($jsCurves)) {
return '
<div id="flot' . self::$poolId . '" style="width:' . $this->width . 'px;height:' . $this->height . 'px"></div>
<script type="text/javascript">
$(function () {
$.plot($(\'#flot' . self::$poolId . '\'), [' . implode(',', $jsCurves) . '], {' . ($options ?? '') . '});
});
</script>';
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
interface ChecksumInterface
{
/**
* @param object $object Checksum target
*
* @return string
*/
public function generateChecksum($object);
}

545
classes/Combination.php Normal file
View File

@@ -0,0 +1,545 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Core\Domain\Combination\CombinationSettings;
use PrestaShop\PrestaShop\Core\Domain\Product\Stock\ValueObject\OutOfStockType;
/**
* Class CombinationCore.
*/
class CombinationCore extends ObjectModel
{
/** @var int Product ID */
public $id_product;
public $reference;
/**
* @deprecated since 8.1.0
*
* @var string
*/
public $supplier_reference;
public $ean13;
public $isbn;
public $upc;
public $mpn;
public $wholesale_price;
public $price;
public $unit_price_impact;
public $ecotax;
public $minimal_quantity = 1;
/** @var int|null Low stock for mail alert */
public $low_stock_threshold = null;
/** @var bool Low stock mail alert activated */
public $low_stock_alert = false;
public $weight;
/** @var bool|null */
public $default_on;
public $available_date = '0000-00-00';
/** @var string|array Text when in stock or array of text by id_lang */
public $available_now;
/** @var string|array Text when not in stock but available to order or array of text by id_lang */
public $available_later;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'product_attribute',
'primary' => 'id_product_attribute',
'multilang' => true,
'fields' => [
'id_product' => ['type' => self::TYPE_INT, 'shop' => 'both', 'validate' => 'isUnsignedId', 'required' => true],
'ean13' => ['type' => self::TYPE_STRING, 'validate' => 'isEan13', 'size' => 13],
'isbn' => ['type' => self::TYPE_STRING, 'validate' => 'isIsbn', 'size' => 32],
'upc' => ['type' => self::TYPE_STRING, 'validate' => 'isUpc', 'size' => 12],
'mpn' => ['type' => self::TYPE_STRING, 'validate' => 'isMpn', 'size' => 40],
'reference' => ['type' => self::TYPE_STRING, 'size' => 64],
'supplier_reference' => ['type' => self::TYPE_STRING, 'size' => 64],
/* Shop fields */
'wholesale_price' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isNegativePrice', 'size' => 27],
'price' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isNegativePrice', 'size' => 20],
'ecotax' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice', 'size' => 20],
'weight' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isFloat'],
'unit_price_impact' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isNegativePrice', 'size' => 20],
'minimal_quantity' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isPositiveInt', 'required' => true],
'low_stock_threshold' => ['type' => self::TYPE_INT, 'shop' => true, 'allow_null' => true, 'validate' => 'isInt'],
'low_stock_alert' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
'default_on' => ['type' => self::TYPE_BOOL, 'allow_null' => true, 'shop' => true, 'validate' => 'isBool'],
'available_date' => ['type' => self::TYPE_DATE, 'shop' => true, 'validate' => 'isDateFormat'],
/* Lang fields */
'available_now' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => CombinationSettings::MAX_AVAILABLE_NOW_LABEL_LENGTH],
'available_later' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'IsGenericName', 'size' => CombinationSettings::MAX_AVAILABLE_LATER_LABEL_LENGTH],
],
];
protected $webserviceParameters = [
'objectNodeName' => 'combination',
'objectsNodeName' => 'combinations',
'fields' => [
'id_product' => ['required' => true, 'xlink_resource' => 'products'],
],
'associations' => [
'product_option_values' => ['resource' => 'product_option_value'],
'images' => ['resource' => 'image', 'api' => 'images/products'],
],
];
/**
* Deletes current Combination from the database.
*
* @return bool True if delete was successful
*
* @throws PrestaShopException
*/
public function delete()
{
if (!parent::delete()) {
return false;
}
$shopIdsList = $this->getShopIdsList();
// Removes the product from StockAvailable for the related shops
if (!empty($shopIdsList)) {
foreach ($shopIdsList as $shopId) {
StockAvailable::removeProductFromStockAvailable((int) $this->id_product, (int) $this->id, $shopId);
}
} else {
StockAvailable::removeProductFromStockAvailable((int) $this->id_product, (int) $this->id);
}
if ($specificPrices = SpecificPrice::getByProductId((int) $this->id_product, (int) $this->id)) {
foreach ($specificPrices as $specificPrice) {
$price = new SpecificPrice((int) $specificPrice['id_specific_price']);
$price->delete();
}
}
if (!$this->hasMultishopEntries() && !$this->deleteAssociations()) {
return false;
}
if (!$this->deleteCartProductCombination()) {
return false;
}
$this->deleteFromSupplier($this->id_product);
$this->deleteFromPack();
Product::updateDefaultAttribute($this->id_product);
return true;
}
/**
* Delete from Supplier.
*
* @param int $idProduct Product ID
*
* @return bool
*/
public function deleteFromSupplier($idProduct)
{
if ($this->hasMultishopEntries()) {
return true;
}
return Db::getInstance()->delete('product_supplier', 'id_product = ' . (int) $idProduct
. ' AND id_product_attribute = ' . (int) $this->id);
}
/**
* Delete association with Pack.
*
* @return bool
*/
protected function deleteFromPack(): bool
{
if ($this->hasMultishopEntries()) {
return true;
}
return Db::getInstance()->delete('pack', 'id_product_item = ' . (int) $this->id_product
. ' AND id_product_attribute_item = ' . (int) $this->id);
}
/**
* Adds current Combination as a new Object to the database.
*
* @param bool $autoDate Automatically set `date_upd` and `date_add` columns
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Indicates whether the Combination has been successfully added
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function add($autoDate = true, $nullValues = false)
{
if ($this->default_on) {
$this->default_on = true;
} else {
$this->default_on = null;
}
if (!parent::add($autoDate, $nullValues)) {
return false;
}
$product = new Product((int) $this->id_product);
$shopIdsList = $this->getShopIdsList();
if ($product->getType() == Product::PTYPE_VIRTUAL) {
$outOfStock = OutOfStockType::OUT_OF_STOCK_AVAILABLE;
} else {
$outOfStock = StockAvailable::outOfStock((int) $this->id_product);
}
if (!empty($shopIdsList)) {
foreach ($shopIdsList as $shopId) {
StockAvailable::setProductOutOfStock((int) $this->id_product, $outOfStock, $shopId, (int) $this->id);
}
} else {
// This creates stock_available for combination as a side effect
StockAvailable::setProductOutOfStock((int) $this->id_product, $outOfStock, $this->id_shop, $this->id);
}
SpecificPriceRule::applyAllRules([(int) $this->id_product]);
Product::updateDefaultAttribute($this->id_product);
return true;
}
/**
* Updates the current Combination in the database.
*
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Indicates whether the Combination has been successfully updated
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function update($nullValues = false)
{
if ($this->default_on) {
$this->default_on = true;
} else {
$this->default_on = null;
}
$return = parent::update($nullValues);
Product::updateDefaultAttribute($this->id_product);
return $return;
}
/**
* Delete associations.
*
* @return bool Indicates whether associations have been successfully deleted
*/
public function deleteAssociations()
{
if ((int) $this->id === 0) {
return false;
}
$result = Db::getInstance()->delete(
'product_attribute_combination',
'`id_product_attribute` = ' . (int) $this->id
);
$result = $result && Db::getInstance()->delete(
'product_attribute_image',
'`id_product_attribute` = ' . (int) $this->id
);
if ($result) {
Hook::exec('actionAttributeCombinationDelete', ['id_product_attribute' => (int) $this->id]);
}
return $result;
}
/**
* Delete product combination from cart.
*
* @return bool
*/
protected function deleteCartProductCombination(): bool
{
if ((int) $this->id === 0) {
return false;
}
if ($this->hasMultishopEntries()) {
$shopIdList = $this->getShopIdsList();
return Db::getInstance()->delete('cart_product', 'id_product_attribute = ' . (int) $this->id . ' AND id_shop IN (' . implode(',', $shopIdList) . ')');
}
return Db::getInstance()->delete('cart_product', 'id_product_attribute = ' . (int) $this->id);
}
/**
* @param array $idsAttribute
*
* @return bool
*/
public function setAttributes($idsAttribute)
{
$result = $this->deleteAssociations();
if ($result && !empty($idsAttribute)) {
$sqlValues = [];
foreach ($idsAttribute as $value) {
$sqlValues[] = '(' . (int) $value . ', ' . (int) $this->id . ')';
}
$result = Db::getInstance()->execute(
'
INSERT INTO `' . _DB_PREFIX_ . 'product_attribute_combination` (`id_attribute`, `id_product_attribute`)
VALUES ' . implode(',', $sqlValues)
);
if ($result) {
Hook::exec('actionAttributeCombinationSave', ['id_product_attribute' => (int) $this->id, 'id_attributes' => $idsAttribute]);
}
}
return $result;
}
/**
* @param array $values
*
* @return bool
*/
public function setWsProductOptionValues($values)
{
$idsAttributes = [];
foreach ($values as $value) {
$idsAttributes[] = $value['id'];
}
return $this->setAttributes($idsAttributes);
}
/**
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public function getWsProductOptionValues()
{
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT a.id_attribute AS id
FROM `' . _DB_PREFIX_ . 'product_attribute_combination` a
' . Shop::addSqlAssociation('attribute', 'a') . '
WHERE a.id_product_attribute = ' . (int) $this->id);
return $result;
}
/**
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public function getWsImages()
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT a.`id_image` as id
FROM `' . _DB_PREFIX_ . 'product_attribute_image` a
' . Shop::addSqlAssociation('product_attribute', 'a') . '
WHERE a.`id_product_attribute` = ' . (int) $this->id . '
');
}
/**
* @param array<int> $idsImage
*
* @return bool
*/
public function setImages($idsImage)
{
if (Db::getInstance()->execute('
DELETE FROM `' . _DB_PREFIX_ . 'product_attribute_image`
WHERE `id_product_attribute` = ' . (int) $this->id) === false) {
return false;
}
if (is_array($idsImage) && count($idsImage)) {
$sqlValues = [];
foreach ($idsImage as $value) {
$sqlValues[] = '(' . (int) $this->id . ', ' . (int) $value . ')';
}
Db::getInstance()->execute(
'INSERT INTO `' . _DB_PREFIX_ . 'product_attribute_image` (`id_product_attribute`, `id_image`)
VALUES ' . implode(',', $sqlValues)
);
}
return true;
}
/**
* @param array<array{id: int}> $values
*
* @return bool
*/
public function setWsImages($values)
{
$idsImages = [];
foreach ($values as $value) {
$idsImages[] = (int) $value['id'];
}
return $this->setImages($idsImages);
}
/**
* @param int $idLang
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public function getAttributesName($idLang)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT al.*
FROM ' . _DB_PREFIX_ . 'product_attribute_combination pac
JOIN ' . _DB_PREFIX_ . 'attribute_lang al ON (pac.id_attribute = al.id_attribute AND al.id_lang=' . (int) $idLang . ')
WHERE pac.id_product_attribute=' . (int) $this->id);
}
/**
* This method is allow to know if a feature is active.
*
* @return bool
*/
public static function isFeatureActive()
{
static $feature_active = null;
if ($feature_active === null) {
$feature_active = (bool) Configuration::get('PS_COMBINATION_FEATURE_ACTIVE');
}
return $feature_active;
}
/**
* This method is allow to know if a Combination entity is currently used.
*
* @param string|null $table Name of table linked to entity
* @param bool $hasActiveColumn True if the table has an active column
*
* @return bool
*/
public static function isCurrentlyUsed($table = null, $hasActiveColumn = false)
{
return parent::isCurrentlyUsed('product_attribute');
}
public static function getIdByEan13($ean13)
{
return self::getIdByGtin($ean13);
}
/**
* For a given gtin reference, returns the corresponding id.
*
* @param string $gtin
*
* @return int|string Product attribute identifier
*/
public static function getIdByGtin($gtin)
{
if (empty($gtin)) {
return 0;
}
if (!Validate::isGtin($gtin)) {
return 0;
}
$query = new DbQuery();
$query->select('pa.id_product_attribute');
$query->from('product_attribute', 'pa');
$query->where('pa.ean13 = \'' . pSQL($gtin) . '\'');
return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query);
}
/**
* For a given product_attribute reference, returns the corresponding id.
*
* @param int $idProduct
* @param string $reference
*
* @return int ID
*/
public static function getIdByReference($idProduct, $reference)
{
if (empty($reference)) {
return 0;
}
$query = new DbQuery();
$query->select('pa.id_product_attribute');
$query->from('product_attribute', 'pa');
$query->where('pa.reference = \'' . pSQL($reference) . '\'');
$query->where('pa.id_product = ' . (int) $idProduct);
return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query);
}
/**
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public function getColorsAttributes()
{
return Db::getInstance()->executeS('
SELECT a.id_attribute
FROM ' . _DB_PREFIX_ . 'product_attribute_combination pac
JOIN ' . _DB_PREFIX_ . 'attribute a ON (pac.id_attribute = a.id_attribute)
JOIN ' . _DB_PREFIX_ . 'attribute_group ag ON (ag.id_attribute_group = a.id_attribute_group)
WHERE pac.id_product_attribute=' . (int) $this->id . ' AND ag.is_color_group = 1
');
}
/**
* Retrive the price of combination.
*
* @param int $idProductAttribute
*
* @return string
*/
public static function getPrice($idProductAttribute)
{
return (string) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
'SELECT product_attribute_shop.`price`
FROM `' . _DB_PREFIX_ . 'product_attribute` pa
' . Shop::addSqlAssociation('product_attribute', 'pa') . '
WHERE pa.`id_product_attribute` = ' . (int) $idProductAttribute
);
}
}

748
classes/Configuration.php Normal file
View File

@@ -0,0 +1,748 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShopBundle\Form\Admin\Type\FormattedTextareaType;
/**
* Class ConfigurationCore.
*/
class ConfigurationCore extends ObjectModel
{
public $id;
/** @var string Key */
public $name;
public $id_shop_group;
public $id_shop;
/** @var string|array<string> Value */
public $value;
/** @var string Object creation date */
public $date_add;
/** @var string Object last modification date */
public $date_upd;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'configuration',
'primary' => 'id_configuration',
'multilang' => true,
'fields' => [
'name' => ['type' => self::TYPE_STRING, 'validate' => 'isConfigName', 'required' => true, 'size' => 254],
'id_shop_group' => ['type' => self::TYPE_NOTHING, 'validate' => 'isUnsignedId'],
'id_shop' => ['type' => self::TYPE_NOTHING, 'validate' => 'isUnsignedId'],
'value' => ['type' => self::TYPE_STRING, 'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
],
];
/** @var array|null Configuration cache (kept for backward compat) */
protected static $_cache = null;
/** @var array|null Configuration cache with optimised key order */
protected static $_new_cache_shop = null;
protected static $_new_cache_group = null;
protected static $_new_cache_global = null;
protected static $_initialized = false;
/** @var array Vars types */
protected static $types = [];
protected $webserviceParameters = [
'fields' => [
'value' => [],
],
];
/**
* @see ObjectModel::getFieldsLang()
*
* @return bool|array Multilingual fields
*/
public function getFieldsLang()
{
if (!is_array($this->value)) {
return true;
}
return parent::getFieldsLang();
}
/**
* Return ID a configuration key.
*
* @param string $key
* @param int $idShopGroup
* @param int $idShop
*
* @return int Configuration key ID
*/
public static function getIdByName($key, $idShopGroup = null, $idShop = null)
{
if ($idShop === null) {
$idShop = Shop::getContextShopID(true);
}
if ($idShopGroup === null) {
$idShopGroup = Shop::getContextShopGroupID(true);
}
return self::getIdByNameFromGivenContext($key, $idShopGroup, $idShop);
}
/**
* @param string $key
* @param int|null $idShopGroup
* @param int|null $idShop
*
* @return int Configuration key ID
*/
public static function getIdByNameFromGivenContext(string $key, ?int $idShopGroup, ?int $idShop): int
{
$sql = 'SELECT `' . bqSQL(self::$definition['primary']) . '`
FROM `' . _DB_PREFIX_ . bqSQL(self::$definition['table']) . '`
WHERE name = \'' . pSQL($key) . '\'
' . Configuration::sqlRestriction($idShopGroup, $idShop);
return (int) Db::getInstance()->getValue($sql);
}
/**
* Is the configuration loaded.
*
* @return bool `true` if configuration is loaded
*/
public static function configurationIsLoaded()
{
return self::$_initialized;
}
/**
* @deprecated 8.0.0 Use resetStaticCache method instead.
*/
public static function clearConfigurationCacheForTesting()
{
self::resetStaticCache();
}
/**
* WARNING: For testing only. Do NOT rely on this method, it may be removed at any time.
*
* @todo Delegate static calls from Configuration to an instance
* of a class to be created.
*/
public static function resetStaticCache()
{
self::$_cache = null;
self::$_new_cache_shop = null;
self::$_new_cache_group = null;
self::$_new_cache_global = null;
self::$_initialized = false;
}
/**
* Load all configuration data.
*/
public static function loadConfiguration()
{
$sql = 'SELECT c.`name`, cl.`id_lang`, IF(cl.`id_lang` IS NULL, c.`value`, cl.`value`) AS value, c.id_shop_group, c.id_shop
FROM `' . _DB_PREFIX_ . bqSQL(self::$definition['table']) . '` c
LEFT JOIN `' . _DB_PREFIX_ . bqSQL(self::$definition['table']) . '_lang` cl ON (c.`' . bqSQL(
self::$definition['primary']
) . '` = cl.`' . bqSQL(self::$definition['primary']) . '`)';
$db = Db::getInstance();
$results = $db->executeS($sql);
if ($results) {
foreach ($results as $row) {
$lang = ($row['id_lang']) ? $row['id_lang'] : 0;
self::$types[$row['name']] = (bool) $lang;
if (!isset(self::$_cache[self::$definition['table']][$lang])) {
self::$_cache[self::$definition['table']][$lang] = [
'global' => [],
'group' => [],
'shop' => [],
];
}
if ($row['value'] === null) {
$row['value'] = '';
}
if ($row['id_shop']) {
self::$_cache[self::$definition['table']][$lang]['shop'][$row['id_shop']][$row['name']] = $row['value'];
self::$_new_cache_shop[$row['name']][$lang][$row['id_shop']] = $row['value'];
} elseif ($row['id_shop_group']) {
self::$_cache[self::$definition['table']][$lang]['group'][$row['id_shop_group']][$row['name']] = $row['value'];
self::$_new_cache_group[$row['name']][$lang][$row['id_shop_group']] = $row['value'];
} else {
self::$_cache[self::$definition['table']][$lang]['global'][$row['name']] = $row['value'];
self::$_new_cache_global[$row['name']][$lang] = $row['value'];
}
}
self::$_initialized = true;
}
}
/**
* Get a single configuration value (in one language only).
*
* @param string $key Key wanted
* @param int $idLang Language ID
* @param int $idShopGroup Shop Group ID
* @param int $idShop Shop ID
* @param mixed $default Default value
*
* @return string|false Value
*/
public static function get($key, $idLang = null, $idShopGroup = null, $idShop = null, $default = false)
{
// Init the cache on demand
if (!self::$_initialized) {
Configuration::loadConfiguration();
}
$idLang = self::isLangKey($key) ? (int) $idLang : 0;
if (self::$_new_cache_shop === null) {
$idShop = 0;
} elseif ($idShop === null || !Shop::isFeatureActive()) {
$idShop = Shop::getContextShopID(true);
}
if (self::$_new_cache_group === null) {
$idShopGroup = 0;
} elseif ($idShopGroup === null || !Shop::isFeatureActive()) {
$idShopGroup = Shop::getContextShopGroupID(true);
}
if ($idShop && Configuration::hasKey($key, $idLang, null, $idShop)) {
return self::$_new_cache_shop[$key][$idLang][$idShop];
} elseif ($idShopGroup && Configuration::hasKey($key, $idLang, $idShopGroup)) {
return self::$_new_cache_group[$key][$idLang][$idShopGroup];
} elseif (Configuration::hasKey($key, $idLang)) {
return self::$_new_cache_global[$key][$idLang];
}
return $default;
}
/**
* Get global value.
*
* @param string $key Configuration key
* @param int|null $idLang Language ID
*
* @return string
*/
public static function getGlobalValue($key, $idLang = null)
{
return Configuration::get($key, $idLang, 0, 0);
}
/**
* Get a single configuration value (in multiple languages).
*
* @param string $key Configuration Key
* @param int $idShopGroup Shop Group ID
* @param int $idShop Shop ID
*
* @return array Values in multiple languages
*/
public static function getConfigInMultipleLangs($key, $idShopGroup = null, $idShop = null)
{
$resultsArray = [];
foreach (Language::getIDs() as $idLang) {
$resultsArray[$idLang] = Configuration::get($key, $idLang, $idShopGroup, $idShop);
}
return $resultsArray;
}
/**
* Get a single configuration value for all shops.
*
* @param string $key Key wanted
* @param int $idLang
*
* @return array Values for all shops
*/
public static function getMultiShopValues($key, $idLang = null)
{
$shops = Shop::getShops(false, null, true);
$resultsArray = [];
foreach ($shops as $idShop) {
$resultsArray[$idShop] = Configuration::get($key, $idLang, null, $idShop);
}
return $resultsArray;
}
/**
* Get several configuration values (in one language only).
*
* @param array $keys Keys wanted
* @param int $idLang Language ID
* @param int $idShopGroup
* @param int $idShop
*
* @return array Values
*
* @throws PrestaShopException
*/
public static function getMultiple($keys, $idLang = null, $idShopGroup = null, $idShop = null)
{
if (!is_array($keys)) {
throw new PrestaShopException('keys var is not an array');
}
$idLang = (int) $idLang;
if ($idShop === null) {
$idShop = Shop::getContextShopID(true);
}
if ($idShopGroup === null) {
$idShopGroup = Shop::getContextShopGroupID(true);
}
$results = [];
foreach ($keys as $key) {
$results[$key] = Configuration::get($key, $idLang, $idShopGroup, $idShop);
}
return $results;
}
/**
* Check if key exists in configuration.
*
* @param mixed $key
* @param mixed|null $idLang
* @param int|null $idShopGroup
* @param int|null $idShop
*
* @return bool
*/
public static function hasKey($key, $idLang = null, $idShopGroup = null, $idShop = null)
{
if (!is_int($key) && !is_string($key)) {
return false;
}
$idLang = (int) $idLang;
if ($idShop) {
return isset(self::$_new_cache_shop[$key][$idLang][$idShop]);
} elseif ($idShopGroup) {
return isset(self::$_new_cache_group[$key][$idLang][$idShopGroup]);
}
return isset(self::$_new_cache_global[$key][$idLang]);
}
/**
* Set TEMPORARY a single configuration value (in one language only).
*
* @param string $key Configuration key
* @param mixed $values `$values` is an array if the configuration is multilingual, a single string else
* @param int $idShopGroup
* @param int $idShop
*/
public static function set($key, $values, $idShopGroup = null, $idShop = null)
{
if (!Validate::isConfigName($key)) {
throw new PrestaShopException(Context::getContext()->getTranslator()->trans('[%s] is not a valid configuration key', [Tools::htmlentitiesUTF8($key)], 'Admin.Notifications.Error'));
}
if ($idShop === null) {
$idShop = (int) Shop::getContextShopID(true);
}
if ($idShopGroup === null) {
$idShopGroup = (int) Shop::getContextShopGroupID(true);
}
if (!is_array($values)) {
$values = [$values];
}
foreach ($values as $lang => $value) {
if ($idShop) {
self::$_new_cache_shop[$key][$lang][$idShop] = $value;
self::$_cache[self::$definition['table']][$lang]['shop'][$idShop][$key] = $value;
} elseif ($idShopGroup) {
self::$_new_cache_group[$key][$lang][$idShopGroup] = $value;
self::$_cache[self::$definition['table']][$lang]['group'][$idShopGroup][$key] = $value;
} else {
self::$_new_cache_global[$key][$lang] = $value;
self::$_cache[self::$definition['table']][$lang]['global'][$key] = $value;
}
}
}
/**
* Update configuration key for global context only.
*
* @param string $key
* @param mixed $values
* @param bool $html
*
* @return bool
*/
public static function updateGlobalValue($key, $values, $html = false)
{
return Configuration::updateValue($key, $values, $html, 0, 0);
}
/**
* Update configuration key and value into database (automatically insert if key does not exist).
*
* Values are inserted/updated directly using SQL, because using (Configuration) ObjectModel
* may not insert values correctly (for example, HTML is escaped, when it should not be).
*
* @TODO Fix saving HTML values in Configuration model
*
* @param string $key Configuration key
* @param mixed $values $values is an array if the configuration is multilingual, a single string else
* @param bool $html Specify if html is authorized in value
* @param int $idShopGroup
* @param int $idShop
*
* @return bool Update result
*/
public static function updateValue($key, $values, $html = false, $idShopGroup = null, $idShop = null)
{
Hook::exec('actionConfigurationUpdateValueBefore', [
'key' => $key,
'values' => $values,
'html' => $html,
'idShopGroup' => $idShopGroup,
'idShop' => $idShop,
]);
if (!Validate::isConfigName($key)) {
throw new PrestaShopException(Context::getContext()->getTranslator()->trans('[%s] is not a valid configuration key', [Tools::htmlentitiesUTF8($key)], 'Admin.Notifications.Error'));
}
if ($idShop === null || !Shop::isFeatureActive()) {
$idShop = Shop::getContextShopID(true);
}
if ($idShopGroup === null || !Shop::isFeatureActive()) {
$idShopGroup = Shop::getContextShopGroupID(true);
}
if (!is_array($values)) {
$values = [$values];
}
if ($html) {
$values = array_map(function ($v) {
return Tools::purifyHTML($v);
}, $values);
}
$result = true;
foreach ($values as $lang => $value) {
$storedValue = Configuration::get($key, $lang, $idShopGroup, $idShop);
// if there isn't a $stored_value, we must insert $value
if (
((!is_numeric($value) && $value === $storedValue) || (is_numeric($value) && $value == $storedValue))
&& Configuration::hasKey($key, $lang, $idShopGroup, $idShop)
) {
continue;
}
// If key already exists, update value
if (Configuration::hasKey($key, $lang, $idShopGroup, $idShop)) {
if (!$lang) {
// Update config not linked to lang
$result &= Db::getInstance()->update(self::$definition['table'], [
'value' => pSQL($value, $html),
'date_upd' => date('Y-m-d H:i:s'),
], '`name` = \'' . pSQL($key) . '\'' . Configuration::sqlRestriction($idShopGroup, $idShop), 1, true);
} else {
// Update multi lang
$sql = 'UPDATE `' . _DB_PREFIX_ . bqSQL(self::$definition['table']) . '_lang` cl
SET cl.value = \'' . pSQL($value, $html) . '\',
cl.date_upd = NOW()
WHERE cl.id_lang = ' . (int) $lang . '
AND cl.`' . bqSQL(self::$definition['primary']) . '` = (
SELECT c.`' . bqSQL(self::$definition['primary']) . '`
FROM `' . _DB_PREFIX_ . bqSQL(self::$definition['table']) . '` c
WHERE c.name = \'' . pSQL($key) . '\''
. Configuration::sqlRestriction($idShopGroup, $idShop)
. ')';
$result &= Db::getInstance()->execute($sql);
}
} else {
// If key does not exists, create it
if (!$configID = Configuration::getIdByName($key, $idShopGroup, $idShop)) {
$now = date('Y-m-d H:i:s');
$data = [
'id_shop_group' => $idShopGroup ? (int) $idShopGroup : null,
'id_shop' => $idShop ? (int) $idShop : null,
'name' => pSQL($key),
'value' => $lang ? null : pSQL($value, $html),
'date_add' => $now,
'date_upd' => $now,
];
$result &= Db::getInstance()->insert(self::$definition['table'], $data, true);
$configID = Db::getInstance()->Insert_ID();
}
if ($lang) {
$table = self::$definition['table'] . '_lang';
$selectConfiguration = strtr(
'SELECT 1 FROM {{ table }} WHERE id_lang = {{ lang }} ' .
'AND `{{ primary_key_column }}` = {{ config_id }}',
[
'{{ table }}' => _DB_PREFIX_ . $table,
'{{ lang }}' => (int) $lang,
'{{ primary_key_column }}' => self::$definition['primary'],
'{{ config_id }}' => $configID,
]
);
$results = Db::getInstance()->getRow($selectConfiguration);
$configurationExists = is_array($results) && count($results) > 0;
$now = date('Y-m-d H:i:s');
$sanitizedValue = pSQL($value, $html);
if ($configurationExists) {
$condition = strtr(
'`{{ primary_key_column }}` = {{ config_id }} AND ' .
'date_upd = "{{ update_date }}" AND ' .
'value = "{{ value }}"',
[
'{{ primary_key_column }}' => self::$definition['primary'],
'{{ config_id }}' => $configID,
'{{ update_date }}' => $now,
'{{ value }}' => $sanitizedValue,
]
);
$result &= Db::getInstance()->update($table, [
'value' => $sanitizedValue,
'date_upd' => date('Y-m-d H:i:s'),
], $condition, 1, true);
} else {
$result &= Db::getInstance()->insert($table, [
self::$definition['primary'] => $configID,
'id_lang' => (int) $lang,
'value' => $sanitizedValue,
'date_upd' => $now,
]);
}
}
}
}
Configuration::set($key, $values, $idShopGroup, $idShop);
return (bool) $result;
}
/**
* Delete a configuration key in database (with or without language management).
*
* @param string $key Key to delete
*
* @return bool Deletion result
*/
public static function deleteByName($key)
{
if (!Validate::isConfigName($key)) {
return false;
}
$result = Db::getInstance()->execute('
DELETE FROM `' . _DB_PREFIX_ . bqSQL(self::$definition['table']) . '_lang`
WHERE `' . bqSQL(self::$definition['primary']) . '` IN (
SELECT `' . bqSQL(self::$definition['primary']) . '`
FROM `' . _DB_PREFIX_ . bqSQL(self::$definition['table']) . '`
WHERE `name` = "' . pSQL($key) . '"
)');
$result2 = Db::getInstance()->execute('
DELETE FROM `' . _DB_PREFIX_ . bqSQL(self::$definition['table']) . '`
WHERE `name` = "' . pSQL($key) . '"');
self::$_cache = null;
self::$_new_cache_shop = null;
self::$_new_cache_group = null;
self::$_new_cache_global = null;
self::$_initialized = false;
return $result && $result2;
}
/**
* Delete configuration key from current context
*
* @param string $key
* @param int $idShopGroup
* @param int $idShop
*/
public static function deleteFromContext($key, ?int $idShopGroup = null, ?int $idShop = null)
{
if (Shop::getContext() == Shop::CONTEXT_ALL) {
return;
}
$idShopGroup = $idShopGroup ?? Shop::getContextShopGroupID(true);
if (!isset($idShop) && Shop::getContext() == Shop::CONTEXT_SHOP) {
$idShop = Shop::getContextShopID(true);
}
$configurationId = Configuration::getIdByName($key, $idShopGroup, $idShop);
self::deleteById($configurationId);
}
/**
* @param string $key
* @param int|null $idShopGroup
* @param int|null $idShop
*/
public static function deleteFromGivenContext(string $key, ?int $idShopGroup, ?int $idShop): void
{
$configurationId = Configuration::getIdByNameFromGivenContext($key, $idShopGroup, $idShop);
self::deleteById($configurationId);
}
public static function deleteById(int $configurationId): void
{
Db::getInstance()->execute('
DELETE FROM `' . _DB_PREFIX_ . bqSQL(self::$definition['table']) . '`
WHERE `' . bqSQL(self::$definition['primary']) . '` = ' . $configurationId);
Db::getInstance()->execute('
DELETE FROM `' . _DB_PREFIX_ . bqSQL(self::$definition['table']) . '_lang`
WHERE `' . bqSQL(self::$definition['primary']) . '` = ' . $configurationId);
self::$_cache = null;
self::$_new_cache_shop = null;
self::$_new_cache_group = null;
self::$_new_cache_global = null;
self::$_initialized = false;
}
/**
* Check if configuration var is defined in given context.
*
* @param string $key
* @param int|null $idLang
* @param int $context
*/
public static function hasContext($key, $idLang, $context)
{
$idShopGroup = $context == Shop::CONTEXT_GROUP && Shop::getContext() != Shop::CONTEXT_ALL ? Shop::getContextShopGroupID(true) : null;
$idShop = $context == Shop::CONTEXT_SHOP && Shop::getContext() == Shop::CONTEXT_SHOP ? Shop::getContextShopID(true) : null;
return Configuration::hasKey($key, $idLang, $idShopGroup, $idShop);
}
/**
* @param string $key
*
* @return bool
*/
public static function isOverridenByCurrentContext($key)
{
if (Configuration::isLangKey($key)) {
$testContext = false;
foreach (Language::getIDs(false) as $idLang) {
if ((Shop::getContext() == Shop::CONTEXT_SHOP && Configuration::hasContext($key, $idLang, Shop::CONTEXT_SHOP))
|| (Shop::getContext() == Shop::CONTEXT_GROUP && Configuration::hasContext($key, $idLang, Shop::CONTEXT_GROUP))) {
$testContext = true;
}
}
} else {
$testContext = ((Shop::getContext() == Shop::CONTEXT_SHOP && Configuration::hasContext($key, null, Shop::CONTEXT_SHOP))
|| (Shop::getContext() == Shop::CONTEXT_GROUP && Configuration::hasContext($key, null, Shop::CONTEXT_GROUP))) ? true : false;
}
return Shop::isFeatureActive() && Shop::getContext() != Shop::CONTEXT_ALL && $testContext;
}
/**
* Check if a key was loaded as multi lang.
*
* @param string $key
*
* @return bool
*/
public static function isLangKey($key)
{
return isset(self::$types[$key]) && self::$types[$key];
}
/**
* @return bool
*/
public static function isCatalogMode()
{
return Configuration::get('PS_CATALOG_MODE')
|| !Configuration::showPrices()
|| (
is_a(Context::getContext()->controller, 'FrontController')
&& Context::getContext()->controller->getRestrictedCountry() == Country::GEOLOC_CATALOG_MODE
);
}
/**
* @return bool
*/
public static function showPrices()
{
return Group::isFeatureActive() ? (bool) Group::getCurrent()->show_prices : true;
}
/**
* Add SQL restriction on shops for configuration table.
*
* @param int $idShopGroup
* @param int $idShop
*
* @return string
*/
protected static function sqlRestriction($idShopGroup, $idShop)
{
if ($idShop) {
return ' AND id_shop = ' . (int) $idShop;
} elseif ($idShopGroup) {
return ' AND id_shop_group = ' . (int) $idShopGroup . ' AND (id_shop IS NULL OR id_shop = 0)';
} else {
return ' AND (id_shop_group IS NULL OR id_shop_group = 0) AND (id_shop IS NULL OR id_shop = 0)';
}
}
/**
* This method is override to allow TranslatedConfiguration entity.
*
* @param string $sqlJoin
* @param string $sqlFilter
* @param string $sqlSort
* @param string $sqlLimit
*
* @return array
*/
public function getWebserviceObjectList($sqlJoin, $sqlFilter, $sqlSort, $sqlLimit)
{
$query = '
SELECT DISTINCT main.`' . bqSQL($this->def['primary']) . '`
FROM `' . _DB_PREFIX_ . bqSQL($this->def['table']) . '` main
' . $sqlJoin . '
WHERE id_configuration NOT IN (
SELECT id_configuration
FROM `' . _DB_PREFIX_ . bqSQL($this->def['table']) . '_lang`
) ' . $sqlFilter . '
' . ($sqlSort != '' ? $sqlSort : '') . '
' . ($sqlLimit != '' ? $sqlLimit : '');
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query);
}
}

View File

@@ -0,0 +1,298 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class ConfigurationKPICore.
*/
class ConfigurationKPICore extends Configuration
{
public static $definition_backup;
/**
* Set KPI definition.
*/
public static function setKpiDefinition()
{
ConfigurationKPI::$definition_backup = Configuration::$definition;
Configuration::$definition['table'] = 'configuration_kpi';
Configuration::$definition['primary'] = 'id_configuration_kpi';
if (empty(static::$_cache[Configuration::$definition['table']])) {
parent::loadConfiguration();
}
}
/**
* Unset KPI definition.
*/
public static function unsetKpiDefinition()
{
Configuration::$definition = ConfigurationKPI::$definition_backup;
}
/**
* Get ID by name.
*
* @param string $key Configuration key
* @param int|null $idShopGroup ShopGroup ID
* @param int|null $idShop Shop ID
*
* @return int ConfigurationKPI ID
*/
public static function getIdByName($key, $idShopGroup = null, $idShop = null)
{
ConfigurationKPI::setKpiDefinition();
$configurationKpi = parent::getIdByName($key, $idShopGroup, $idShop);
ConfigurationKPI::unsetKpiDefinition();
return $configurationKpi;
}
/**
* Load configuration.
*/
public static function loadConfiguration()
{
ConfigurationKPI::setKpiDefinition();
parent::loadConfiguration();
ConfigurationKPI::unsetKpiDefinition();
}
/**
* Get value.
*
* @param string $key Configuration key
* @param null $idLang Language ID
* @param null $idShopGroup ShopGroup ID
* @param null $idShop Shop ID
* @param bool $default Default value
*
* @return string
*/
public static function get($key, $idLang = null, $idShopGroup = null, $idShop = null, $default = false)
{
ConfigurationKPI::setKpiDefinition();
$value = parent::get($key, $idLang, $idShopGroup, $idShop, $default);
ConfigurationKPI::unsetKpiDefinition();
return $value;
}
/**
* Get global vlaue.
*
* @param string $key Configuration key
* @param int|null $idLang Language ID
*
* @return string Global value
*/
public static function getGlobalValue($key, $idLang = null)
{
ConfigurationKPI::setKpiDefinition();
$globalValue = parent::getGlobalValue($key, $idLang);
ConfigurationKPI::unsetKpiDefinition();
return $globalValue;
}
/**
* Get value independent from language.
*
* @param string $key Configuration key
* @param null $idShopGroup ShopGroup ID
* @param null $idShop Shop ID
*
* @return array Values for key for all available languages
*/
public static function getInt($key, $idShopGroup = null, $idShop = null)
{
ConfigurationKPI::setKpiDefinition();
$values = parent::getConfigInMultipleLangs($key, $idShopGroup, $idShop);
ConfigurationKPI::unsetKpiDefinition();
return $values;
}
/**
* Get multiple keys.
*
* @param array $keys Configuation keys
* @param int|null $idLang Language ID
* @param int|null $idShopGroup ShopGroup ID
* @param int|null $idShop Shop ID
*
* @return array Configuration values
*/
public static function getMultiple($keys, $idLang = null, $idShopGroup = null, $idShop = null)
{
ConfigurationKPI::setKpiDefinition();
$configurationValues = parent::getMultiple($keys, $idLang, $idShopGroup, $idShop);
ConfigurationKPI::unsetKpiDefinition();
return $configurationValues;
}
/**
* Has key.
*
* @param string $key
* @param int|null $idLang Language ID
* @param int|null $idShopGroup ShopGroup ID
* @param int|null $idShop Shop ID
*
* @return bool
*/
public static function hasKey($key, $idLang = null, $idShopGroup = null, $idShop = null)
{
ConfigurationKPI::setKpiDefinition();
$hasKey = parent::hasKey($key, $idLang, $idShopGroup, $idShop);
ConfigurationKPI::unsetKpiDefinition();
return $hasKey;
}
/**
* Set key.
*
* @param string $key Configuration key
* @param mixed $values Values
* @param null $idShopGroup ShopGroup ID
* @param null $idShop Shop ID
*/
public static function set($key, $values, $idShopGroup = null, $idShop = null)
{
ConfigurationKPI::setKpiDefinition();
parent::set($key, $values, $idShopGroup, $idShop);
ConfigurationKPI::unsetKpiDefinition();
}
/**
* Update global value.
*
* @param string $key Configuration key
* @param mixed $values Values
* @param bool $html Do the values contain HTML?
*
* @return bool Indicates whether the key was successfully updated
*/
public static function updateGlobalValue($key, $values, $html = false)
{
ConfigurationKPI::setKpiDefinition();
$updateSuccess = parent::updateGlobalValue($key, $values, $html);
ConfigurationKPI::unsetKpiDefinition();
return $updateSuccess;
}
/**
* Update value.
*
* @param string $key Configuration key
* @param mixed $values Values
* @param bool $html Do the values contain HTML?
* @param null $idShopGroup ShopGroup ID
* @param null $idShop Shop ID
*
* @return bool Indicates whether the key was successfully updated
*/
public static function updateValue($key, $values, $html = false, $idShopGroup = null, $idShop = null)
{
ConfigurationKPI::setKpiDefinition();
$updateSuccess = parent::updateValue($key, $values, $html, $idShopGroup, $idShop);
ConfigurationKPI::unsetKpiDefinition();
return $updateSuccess;
}
/**
* @param string $key
*
* @return bool
*/
public static function deleteByName($key)
{
ConfigurationKPI::setKpiDefinition();
$deleteSuccess = parent::deleteByName($key);
ConfigurationKPI::unsetKpiDefinition();
return $deleteSuccess;
}
/**
* @param string $key
* @param int|null $idShopGroup
* @param int|null $idShop
*
* @return bool
*/
public static function deleteFromContext($key, ?int $idShopGroup = null, ?int $idShop = null)
{
ConfigurationKPI::setKpiDefinition();
$deleteSuccess = parent::deleteFromContext($key, $idShopGroup, $idShop);
ConfigurationKPI::unsetKpiDefinition();
return $deleteSuccess;
}
/**
* @param string $key
* @param int $idLang
* @param int $context
*
* @return bool
*/
public static function hasContext($key, $idLang, $context)
{
ConfigurationKPI::setKpiDefinition();
$hasContext = parent::hasContext($key, $idLang, $context);
ConfigurationKPI::unsetKpiDefinition();
return $hasContext;
}
/**
* @param string $key
*
* @return bool
*/
public static function isOverridenByCurrentContext($key)
{
ConfigurationKPI::setKpiDefinition();
$isOverriden = parent::isOverridenByCurrentContext($key);
ConfigurationKPI::unsetKpiDefinition();
return $isOverriden;
}
/**
* @param string $key
*
* @return bool
*/
public static function isLangKey($key)
{
ConfigurationKPI::setKpiDefinition();
$isLangKey = parent::isLangKey($key);
ConfigurationKPI::unsetKpiDefinition();
return $isLangKey;
}
/**
* @param int $idShopGroup
* @param int $idShop
*
* @return string
*/
protected static function sqlRestriction($idShopGroup, $idShop)
{
ConfigurationKPI::setKpiDefinition();
$sqlRestriction = parent::sqlRestriction($idShopGroup, $idShop);
ConfigurationKPI::unsetKpiDefinition();
return $sqlRestriction;
}
}

View File

@@ -0,0 +1,435 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
class ConfigurationTestCore
{
public static $test_files = [
'/classes/log/index.php',
'/classes/cache/index.php',
'/config/index.php',
'/download/index.php',
'/js/tools.js',
'/js/jquery/plugins/fancybox/jquery.fancybox.js',
'/localization/fr.xml',
'/mails/index.php',
'/modules/index.php',
'/override/index.php',
'/pdf/order-return.tpl',
'/translations/export/index.php',
'/webservice/dispatcher.php',
'/index.php',
'/vendor/autoload.php',
];
/**
* getDefaultTests return an array of tests to executes.
* key are method name, value are parameters (false for no parameter)
* all path are _PS_ROOT_DIR_ related.
*
* @return array
*/
public static function getDefaultTests()
{
return [
'upload' => false,
'cache_dir' => 'var/cache',
'log_dir' => 'var/logs',
'img_dir' => 'img',
'module_dir' => 'modules',
'theme_lang_dir' => 'themes/' . _THEME_NAME_ . '/lang/',
'theme_pdf_lang_dir' => 'themes/' . _THEME_NAME_ . '/pdf/lang/',
'theme_cache_dir' => 'themes/' . _THEME_NAME_ . '/cache/',
'translations_dir' => 'translations',
'customizable_products_dir' => 'upload',
'virtual_products_dir' => 'download',
'config_sf2_dir' => 'app/config',
'translations_sf2' => 'translations',
'system' => [
'fopen', 'fclose', 'fread', 'fwrite',
'rename', 'file_exists', 'unlink', 'rmdir', 'mkdir',
'getcwd', 'chdir', 'chmod',
],
'phpversion' => false,
'apache_mod_rewrite' => false,
'curl' => false,
'gd' => false,
'json' => false,
'pdo_mysql' => false,
'config_dir' => 'config',
'files' => false,
'mails_dir' => 'mails',
'openssl' => false,
'openssl_key_generation' => false,
'simplexml' => false,
'zip' => false,
'fileinfo' => false,
'intl' => false,
'memory_limit' => false,
'mbstring' => false,
];
}
/**
* getDefaultTestsOp return an array of tests to executes.
* key are method name, value are parameters (false for no parameter).
*
* @return array
*/
public static function getDefaultTestsOp()
{
return [
'new_phpversion' => false,
'gz' => false,
'mbstring' => false,
'dom' => false,
'pdo_mysql' => false,
'fopen' => false,
'intl' => false,
'memory_limit' => false,
];
}
/**
* run all test defined in $tests.
*
* @param array $tests
*
* @return array results of tests
*/
public static function check($tests)
{
$res = [];
foreach ($tests as $key => $test) {
$res[$key] = ConfigurationTest::run($key, $test);
}
return $res;
}
public static function run($ptr, $arg = 0)
{
if (call_user_func(['ConfigurationTest', 'test_' . $ptr], $arg)) {
return 'ok';
}
return 'fail';
}
public static function test_phpversion()
{
return version_compare(PHP_VERSION, '7.1.3', '>=');
}
public static function test_apache_mod_rewrite()
{
if (isset($_SERVER['SERVER_SOFTWARE'])
&& strpos(strtolower($_SERVER['SERVER_SOFTWARE']), 'apache') === false || !function_exists('apache_get_modules')) {
return true;
}
return in_array('mod_rewrite', apache_get_modules());
}
public static function test_new_phpversion()
{
return static::test_phpversion();
}
public static function test_mysql_support()
{
return extension_loaded('mysql') || extension_loaded('mysqli') || extension_loaded('pdo_mysql');
}
public static function test_intl()
{
return extension_loaded('intl');
}
public static function test_memory_limit()
{
$memoryLimit = Tools::getMemoryLimit();
return $memoryLimit === '-1' || $memoryLimit >= Tools::getOctets('256M');
}
public static function test_pdo_mysql()
{
return extension_loaded('pdo_mysql');
}
public static function test_upload()
{
return ini_get('file_uploads');
}
public static function test_fopen()
{
return in_array(ini_get('allow_url_fopen'), ['On', 'on', '1']);
}
public static function test_system($funcs)
{
foreach ($funcs as $func) {
if (!function_exists($func)) {
return false;
}
}
return true;
}
public static function test_curl()
{
return extension_loaded('curl');
}
public static function test_gd()
{
if (function_exists('gd_info')) {
$gd = gd_info();
return !empty($gd['JPEG Support']);
}
return false;
}
public static function test_json()
{
return extension_loaded('json');
}
public static function test_gz()
{
if (function_exists('gzencode')) {
return @gzencode('dd') !== false;
}
return false;
}
public static function test_simplexml()
{
return extension_loaded('SimpleXML');
}
public static function test_zip()
{
return extension_loaded('zip');
}
public static function test_fileinfo()
{
return extension_loaded('fileinfo');
}
public static function test_dir($relative_dir, $recursive = false, &$full_report = null)
{
$dir = rtrim(_PS_ROOT_DIR_, '\\/') . DIRECTORY_SEPARATOR . trim($relative_dir, '\\/');
if (!file_exists($dir) || !$dh = @opendir($dir)) {
$full_report = sprintf('Directory %s does not exist or is not writable', $dir); // sprintf for future translation
return false;
}
closedir($dh);
$dummy = rtrim($dir, '\\/') . DIRECTORY_SEPARATOR . uniqid();
if (@file_put_contents($dummy, 'test')) {
@unlink($dummy);
if (!$recursive) {
return true;
}
} elseif (!is_writable($dir)) {
$full_report = sprintf('Directory %s is not writable', $dir); // sprintf for future translation
return false;
}
if ($recursive) {
foreach (Tools::getDirectories($dir) as $file) {
if (!ConfigurationTest::test_dir($relative_dir . DIRECTORY_SEPARATOR . $file, $recursive, $full_report)) {
return false;
}
}
}
return true;
}
public static function test_file($file_relative)
{
$file = _PS_ROOT_DIR_ . DIRECTORY_SEPARATOR . $file_relative;
return file_exists($file) && is_writable($file);
}
public static function test_config_dir($dir)
{
return ConfigurationTest::test_dir($dir);
}
public static function test_sitemap($dir)
{
return ConfigurationTest::test_file($dir);
}
public static function test_root_dir($dir)
{
return ConfigurationTest::test_dir($dir);
}
public static function test_log_dir($dir)
{
return ConfigurationTest::test_dir($dir);
}
public static function test_admin_dir($dir)
{
return ConfigurationTest::test_dir($dir);
}
public static function test_img_dir($dir)
{
return ConfigurationTest::test_dir($dir, true);
}
public static function test_module_dir($dir)
{
return ConfigurationTest::test_dir($dir, true);
}
public static function test_cache_dir($dir)
{
return ConfigurationTest::test_dir($dir, true);
}
public static function test_tools_v2_dir($dir)
{
return ConfigurationTest::test_dir($dir);
}
public static function test_cache_v2_dir($dir)
{
return ConfigurationTest::test_dir($dir);
}
public static function test_download_dir($dir)
{
return ConfigurationTest::test_dir($dir);
}
public static function test_mails_dir($dir)
{
return ConfigurationTest::test_dir($dir, true);
}
public static function test_translations_dir($dir)
{
return ConfigurationTest::test_dir($dir, true);
}
public static function test_config_sf2_dir($dir)
{
return ConfigurationTest::test_dir($dir, true);
}
public static function test_theme_lang_dir($dir)
{
$absoluteDir = rtrim(_PS_ROOT_DIR_, '\\/') . DIRECTORY_SEPARATOR . trim($dir, '\\/');
if (!file_exists($absoluteDir)) {
return true;
}
return ConfigurationTest::test_dir($dir, true);
}
public static function test_theme_pdf_lang_dir($dir)
{
$absoluteDir = rtrim(_PS_ROOT_DIR_, '\\/') . DIRECTORY_SEPARATOR . trim($dir, '\\/');
if (!file_exists($absoluteDir)) {
return true;
}
return ConfigurationTest::test_dir($dir, true);
}
public static function test_theme_cache_dir($dir)
{
$absoluteDir = rtrim(_PS_ROOT_DIR_, '\\/') . DIRECTORY_SEPARATOR . trim($dir, '\\/');
if (!file_exists($absoluteDir)) {
return true;
}
return ConfigurationTest::test_dir($dir, true);
}
public static function test_customizable_products_dir($dir)
{
return ConfigurationTest::test_dir($dir);
}
public static function test_virtual_products_dir($dir)
{
return ConfigurationTest::test_dir($dir);
}
public static function test_mbstring()
{
return extension_loaded('mbstring');
}
public static function test_openssl()
{
return function_exists('openssl_encrypt');
}
public static function test_openssl_key_generation()
{
$privateKey = openssl_pkey_new([
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
if ($privateKey === false) {
return false;
}
return true;
}
public static function test_sessions()
{
return in_array(session_status(), [PHP_SESSION_ACTIVE, PHP_SESSION_NONE], true);
}
public static function test_dom()
{
return extension_loaded('Dom');
}
public static function test_files($full = false)
{
$return = [];
foreach (ConfigurationTest::$test_files as $file) {
if (!file_exists(rtrim(_PS_ROOT_DIR_, DIRECTORY_SEPARATOR) . str_replace('/', DIRECTORY_SEPARATOR, $file))) {
if ($full) {
$return[] = $file;
} else {
return false;
}
}
}
if ($full) {
return $return;
}
return true;
}
public static function test_translations_sf2($dir)
{
return ConfigurationTest::test_dir($dir);
}
}

227
classes/Connection.php Normal file
View File

@@ -0,0 +1,227 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class ConnectionCore.
*/
class ConnectionCore extends ObjectModel
{
/** @var int */
public $id_guest;
/** @var int */
public $id_page;
/** @var string */
public $ip_address;
/** @var string */
public $http_referer;
/** @var int */
public $id_shop;
/** @var int */
public $id_shop_group;
/** @var string */
public $date_add;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'connections',
'primary' => 'id_connections',
'fields' => [
'id_guest' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_page' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'ip_address' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
'http_referer' => ['type' => self::TYPE_STRING, 'validate' => 'isAbsoluteUrl', 'size' => 255],
'id_shop' => ['type' => self::TYPE_INT, 'required' => true],
'id_shop_group' => ['type' => self::TYPE_INT, 'required' => true],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
],
];
/**
* @see ObjectModel::getFields()
*
* @return array
*/
public function getFields()
{
if (!$this->id_shop_group) {
$this->id_shop_group = Context::getContext()->shop->id_shop_group;
}
$fields = parent::getFields();
return $fields;
}
/**
* @param Cookie $cookie
* @param bool $full
*
* @return array
*/
public static function setPageConnection($cookie, $full = true)
{
$idPage = false;
// The connection is created if it does not exist yet and we get the current page id
if (!isset($cookie->id_connections) || !isset($_SERVER['HTTP_REFERER']) || strstr($_SERVER['HTTP_REFERER'], Tools::getHttpHost(false, false) . '/') === false) {
$idPage = Connection::setNewConnection($cookie);
}
// If we do not track the pages, no need to get the page id
if (!Configuration::get('PS_STATSDATA_PAGESVIEWS') && !Configuration::get('PS_STATSDATA_CUSTOMER_PAGESVIEWS')) {
return [];
}
if (!$idPage) {
$idPage = Page::getCurrentId();
}
// If we do not track the page views by customer, the id_page is the only information needed
if (!Configuration::get('PS_STATSDATA_CUSTOMER_PAGESVIEWS')) {
return ['id_page' => $idPage];
}
// The ending time will be updated by an ajax request when the guest will close the page
$timeStart = date('Y-m-d H:i:s');
Db::getInstance()->insert(
'connections_page',
[
'id_connections' => (int) $cookie->id_connections,
'id_page' => (int) $idPage,
'time_start' => $timeStart,
],
false,
true,
Db::INSERT_IGNORE
);
// This array is serialized and used by the ajax request to identify the page
return [
'id_connections' => (int) $cookie->id_connections,
'id_page' => (int) $idPage,
'time_start' => $timeStart,
];
}
/**
* Check if the current visitor is a bot
*
* @return bool
*/
public static function isBot()
{
if (isset($_SERVER['HTTP_USER_AGENT'])
&& preg_match('/SeznamBot|BotLink|ahoy|AlkalineBOT|anthill|appie|arale|araneo|AraybOt|ariadne|arks|ATN_Worldwide|Atomz|bbot|Bjaaland|Ukonline|borg\-bot\/0\.9|boxseabot|bspider|calif|christcrawler|CMC\/0\.01|combine|confuzzledbot|CoolBot|cosmos|Internet Cruiser Robot|cusco|cyberspyder|cydralspider|desertrealm, desert realm|digger|DIIbot|grabber|downloadexpress|DragonBot|dwcp|ecollector|ebiness|elfinbot|esculapio|esther|fastcrawler|FDSE|FELIX IDE|ESI|fido|H<>m<EFBFBD>h<EFBFBD>kki|KIT\-Fireball|fouineur|Freecrawl|gammaSpider|gazz|gcreep|golem|googlebot|griffon|Gromit|gulliver|gulper|hambot|havIndex|hotwired|htdig|iajabot|INGRID\/0\.1|Informant|InfoSpiders|inspectorwww|irobot|Iron33|JBot|jcrawler|Teoma|Jeeves|jobo|image\.kapsi\.net|KDD\-Explorer|ko_yappo_robot|label\-grabber|larbin|legs|Linkidator|linkwalker|Lockon|logo_gif_crawler|marvin|mattie|mediafox|MerzScope|NEC\-MeshExplorer|MindCrawler|udmsearch|moget|Motor|msnbot|muncher|muninn|MuscatFerret|MwdSearch|sharp\-info\-agent|WebMechanic|NetScoop|newscan\-online|ObjectsSearch|Occam|Orbsearch\/1\.0|packrat|pageboy|ParaSite|patric|pegasus|perlcrawler|phpdig|piltdownman|Pimptrain|pjspider|PlumtreeWebAccessor|PortalBSpider|psbot|Getterrobo\-Plus|Raven|RHCS|RixBot|roadrunner|Robbie|robi|RoboCrawl|robofox|Scooter|Search\-AU|searchprocess|Senrigan|Shagseeker|sift|SimBot|Site Valet|skymob|SLCrawler\/2\.0|slurp|ESI|snooper|solbot|speedy|spider_monkey|SpiderBot\/1\.0|spiderline|nil|suke|http:\/\/www\.sygol\.com|tach_bw|TechBOT|templeton|titin|topiclink|UdmSearch|urlck|Valkyrie libwww\-perl|verticrawl|Victoria|void\-bot|Voyager|VWbot_K|crawlpaper|wapspider|WebBandit\/1\.0|webcatcher|T\-H\-U\-N\-D\-E\-R\-S\-T\-O\-N\-E|WebMoose|webquest|webreaper|webs|webspider|WebWalker|wget|winona|whowhere|wlm|WOLP|WWWC|none|XGET|Nederland\.zoek|AISearchBot|woriobot|NetSeer|Nutch|YandexBot/i', $_SERVER['HTTP_USER_AGENT'])) {
return true;
}
return false;
}
/**
* @param Cookie $cookie
*
* @return int|bool Connection ID
* `false` if failure
*/
public static function setNewConnection($cookie)
{
// This is a bot : don't log connection
if (static::isBot()) {
return false;
}
// A new connection is created if the guest made no actions during 30 minutes
$sql = 'SELECT SQL_NO_CACHE `id_guest`
FROM `' . _DB_PREFIX_ . 'connections`
WHERE `id_guest` = ' . (int) $cookie->id_guest . '
AND `date_add` > \'' . pSQL(date('Y-m-d H:i:00', time() - 1800)) . '\'
' . Shop::addSqlRestriction(Shop::SHARE_CUSTOMER) . '
ORDER BY `date_add` DESC';
$result = Db::getInstance()->getRow($sql, false);
if (empty($result['id_guest']) && (int) $cookie->id_guest) {
// The old connections details are removed from the database in order to spare some memory
Connection::cleanConnectionsPages();
$referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
$arrayUrl = parse_url($referer);
if (!isset($arrayUrl['host']) || preg_replace('/^www./', '', $arrayUrl['host']) == preg_replace('/^www./', '', Tools::getHttpHost(false, false))) {
$referer = '';
}
$connection = new Connection();
$connection->id_guest = (int) $cookie->id_guest;
$connection->id_page = Page::getCurrentId();
$connection->ip_address = Tools::getRemoteAddr() ? (int) ip2long(Tools::getRemoteAddr()) : '';
$connection->id_shop = Context::getContext()->shop->id;
$connection->id_shop_group = Context::getContext()->shop->id_shop_group;
$connection->date_add = $cookie->date_add;
if (Validate::isAbsoluteUrl($referer)) {
$connection->http_referer = substr($referer, 0, 254);
}
$connection->add();
$cookie->id_connections = (int) $connection->id;
return $connection->id_page;
}
return false;
}
/**
* @param int $idConnections
* @param int $idPage
* @param string $timeStart
* @param int $time
*/
public static function setPageTime($idConnections, $idPage, $timeStart, $time)
{
if (!Validate::isUnsignedId($idConnections)
|| !Validate::isUnsignedId($idPage)
|| !Validate::isDate($timeStart)) {
return;
}
// Limited to 5 minutes because more than 5 minutes is considered as an error
if ($time > 300000) {
$time = 300000;
}
Db::getInstance()->execute('
UPDATE `' . _DB_PREFIX_ . 'connections_page`
SET `time_end` = `time_start` + INTERVAL ' . (int) ($time / 1000) . ' SECOND
WHERE `id_connections` = ' . (int) $idConnections . '
AND `id_page` = ' . (int) $idPage . '
AND `time_start` = \'' . pSQL($timeStart) . '\'');
}
/**
* Clean connections page.
*/
public static function cleanConnectionsPages()
{
$period = Configuration::get('PS_STATS_OLD_CONNECT_AUTO_CLEAN');
if ($period === 'week') {
$interval = '1 WEEK';
} elseif ($period === 'month') {
$interval = '1 MONTH';
} elseif ($period === 'year') {
$interval = '1 YEAR';
} else {
return;
}
if ($interval != null) {
// Records of connections details older than the beginning of the specified interval are deleted
Db::getInstance()->execute('
DELETE FROM `' . _DB_PREFIX_ . 'connections_page`
WHERE time_start < LAST_DAY(DATE_SUB(NOW(), INTERVAL ' . $interval . '))');
}
}
}

View File

@@ -0,0 +1,107 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class ConnectionsSourceCore.
*/
class ConnectionsSourceCore extends ObjectModel
{
public $id_connections;
public $http_referer;
public $request_uri;
public $keywords;
public $date_add;
public static $uri_max_size = 255;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'connections_source',
'primary' => 'id_connections_source',
'fields' => [
'id_connections' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'http_referer' => ['type' => self::TYPE_STRING, 'validate' => 'isAbsoluteUrl', 'size' => 255],
'request_uri' => ['type' => self::TYPE_STRING, 'validate' => 'isUrl', 'size' => 255],
'keywords' => ['type' => self::TYPE_STRING, 'validate' => 'isMessage', 'size' => 255],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate', 'required' => true],
],
];
public static function logHttpReferer(?Cookie $cookie = null)
{
if (!$cookie) {
$cookie = Context::getContext()->cookie;
}
if (!isset($cookie->id_connections) || !Validate::isUnsignedInt($cookie->id_connections)) {
return false;
}
// If the referrer is not correct, we drop the connection
if (isset($_SERVER['HTTP_REFERER']) && !Validate::isAbsoluteUrl($_SERVER['HTTP_REFERER'])) {
return false;
}
$source = new ConnectionsSource();
$source->request_uri = Tools::getHttpHost();
// There are a few more operations if there is a referrer
if (!empty($_SERVER['HTTP_REFERER'])) {
// If the referrer is internal (i.e. from your own website), then we drop the connection
$parsed = parse_url($_SERVER['HTTP_REFERER']);
$parsedHost = parse_url(Tools::getProtocol() . $source->request_uri . __PS_BASE_URI__);
if (!isset($parsed['host']) || !isset($parsed['path']) || !isset($parsedHost['path'])) {
return false;
}
if (
preg_replace('/^www./', '', $parsed['host']) == preg_replace('/^www./', '', $source->request_uri)
&& !strncmp($parsed['path'], $parsedHost['path'], strlen(__PS_BASE_URI__))
) {
return false;
}
$source->http_referer = substr($_SERVER['HTTP_REFERER'], 0, ConnectionsSource::$uri_max_size);
$source->keywords = substr(trim(SearchEngine::getKeywords($_SERVER['HTTP_REFERER'])), 0, ConnectionsSource::$uri_max_size);
}
$source->id_connections = (int) $cookie->id_connections;
if (isset($_SERVER['REQUEST_URI'])) {
$source->request_uri .= $_SERVER['REQUEST_URI'];
} elseif (isset($_SERVER['REDIRECT_URL'])) {
$source->request_uri .= $_SERVER['REDIRECT_URL'];
}
if (!Validate::isUrl($source->request_uri)) {
$source->request_uri = '';
} else {
$source->request_uri = substr($source->request_uri, 0, ConnectionsSource::$uri_max_size);
}
return $source->add();
}
/**
* Get Order sources.
*
* @param int $idOrder Order ID
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public static function getOrderSources($idOrder)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT cos.`http_referer`, cos.`request_uri`, cos.`keywords`, cos.`date_add`
FROM `' . _DB_PREFIX_ . 'orders` o
INNER JOIN `' . _DB_PREFIX_ . 'guest` g ON g.`id_customer` = o.`id_customer`
INNER JOIN `' . _DB_PREFIX_ . 'connections` co ON co.`id_guest` = g.`id_guest`
INNER JOIN `' . _DB_PREFIX_ . 'connections_source` cos ON cos.`id_connections` = co.`id_connections`
WHERE `id_order` = ' . (int) $idOrder . '
ORDER BY cos.`date_add` DESC');
}
}

105
classes/Contact.php Normal file
View File

@@ -0,0 +1,105 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShopBundle\Form\Admin\Type\FormattedTextareaType;
/**
* Class ContactCore.
*/
class ContactCore extends ObjectModel
{
public $id;
/** @var string|array<int, string> Name */
public $name;
/** @var string E-mail */
public $email;
/** @var string|array<int, string> Detailed description */
public $description;
/** @var bool */
public $customer_service;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'contact',
'primary' => 'id_contact',
'multilang' => true,
'fields' => [
'email' => [
'type' => self::TYPE_STRING,
'validate' => 'isEmail',
'size' => 255,
],
'customer_service' => [
'type' => self::TYPE_BOOL,
'validate' => 'isBool',
],
/* Lang fields */
'name' => [
'type' => self::TYPE_STRING,
'lang' => true,
'validate' => 'isGenericName',
'required' => true,
'size' => 255,
],
'description' => [
'type' => self::TYPE_STRING,
'lang' => true,
'validate' => 'isString',
'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4,
],
],
];
/**
* Return available contacts.
*
* @param int $idLang Language ID
*
* @return array Contacts
*/
public static function getContacts($idLang)
{
$shopIds = Shop::getContextListShopID();
$sql = 'SELECT *
FROM `' . _DB_PREFIX_ . 'contact` c
' . Shop::addSqlAssociation('contact', 'c', false) . '
LEFT JOIN `' . _DB_PREFIX_ . 'contact_lang` cl ON (c.`id_contact` = cl.`id_contact`)
WHERE cl.`id_lang` = ' . (int) $idLang . '
AND contact_shop.`id_shop` IN (' . implode(', ', array_map('intval', $shopIds)) . ')
GROUP BY c.`id_contact`
ORDER BY `name` ASC';
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
}
/**
* Return available categories contacts.
*
* @return array Contacts
*/
public static function getCategoriesContacts()
{
$shopIds = Shop::getContextListShopID();
return Db::getInstance()->executeS('
SELECT cl.*
FROM ' . _DB_PREFIX_ . 'contact ct
' . Shop::addSqlAssociation('contact', 'ct', false) . '
LEFT JOIN ' . _DB_PREFIX_ . 'contact_lang cl
ON (cl.id_contact = ct.id_contact AND cl.id_lang = ' . (int) Context::getContext()->language->id . ')
WHERE ct.customer_service = 1
AND contact_shop.`id_shop` IN (' . implode(', ', array_map('intval', $shopIds)) . ')
GROUP BY ct.`id_contact`
');
}
}

485
classes/Context.php Normal file
View File

@@ -0,0 +1,485 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use Detection\MobileDetect;
use PrestaShop\PrestaShop\Adapter\ContainerFinder;
use PrestaShop\PrestaShop\Adapter\Module\Repository\ModuleRepository;
use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
use PrestaShop\PrestaShop\Core\Context\LegacyControllerContext;
use PrestaShop\PrestaShop\Core\Exception\ContainerNotFoundException;
use PrestaShop\PrestaShop\Core\Localization\CLDR\ComputingPrecision;
use PrestaShop\PrestaShop\Core\Localization\LocaleInterface;
use PrestaShopBundle\Install\Language as InstallLanguage;
use PrestaShopBundle\Translation\TranslatorComponent as Translator;
use PrestaShopBundle\Translation\TranslatorInterface;
use PrestaShopBundle\Translation\TranslatorLanguageLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* Class ContextCore.
*
* This class is responsible for holding all basic information about the environment,
* the customer, cart, currency, language etc.
*/
class ContextCore
{
/** @var Context|null */
protected static $instance;
/** @var Cart|null */
public $cart;
/** @var Customer|null */
public $customer;
/** @var Cookie|null */
public $cookie;
/** @var SessionInterface|null */
public $session;
/** @var Link|null */
public $link;
/** @var Country|null */
public $country;
/** @var Employee|null */
public $employee;
/** @var AdminController|FrontController|LegacyControllerContext|null */
public $controller;
/** @var string */
public $override_controller_name_for_translations;
/** @var Language|InstallLanguage|null */
public $language;
/** @var Currency|null */
public $currency;
/**
* Current locale instance.
*
* @var LocaleInterface|null
*/
public $currentLocale;
/** @var Tab */
public $tab;
/** @var Shop|null */
public $shop;
/** @var Shop */
public $tmpOldShop;
/** @var Smarty|null */
public $smarty;
public ?MobileDetect $mobile_detect = null;
/** @var int */
public $mode;
/** @var ContainerBuilder|ContainerInterface|null */
public $container;
/** @var float */
public $virtualTotalTaxExcluded = 0;
/** @var float */
public $virtualTotalTaxIncluded = 0;
/** @var Translator */
protected $translator = null;
/** @var int */
protected $priceComputingPrecision = null;
/** Mobile device of the customer. */
protected ?bool $mobile_device = null;
protected ?bool $is_mobile = null;
protected ?bool $is_tablet = null;
/** @var int */
public const DEVICE_COMPUTER = 1;
/** @var int */
public const DEVICE_TABLET = 2;
/** @var int */
public const DEVICE_MOBILE = 4;
/** @var int */
public const MODE_STD = 1;
/** @var int */
public const MODE_STD_CONTRIB = 2;
/** @var int */
public const MODE_HOST_CONTRIB = 4;
/** @var int */
public const MODE_HOST = 8;
/** Sets MobileDetect tool object. */
public function getMobileDetect(): MobileDetect
{
if ($this->mobile_detect === null) {
$this->mobile_detect = new MobileDetect();
}
return $this->mobile_detect;
}
/** Checks if visitor's device is a mobile device. */
public function isMobile(): bool
{
if ($this->is_mobile === null) {
$mobileDetect = $this->getMobileDetect();
$this->is_mobile = $mobileDetect->isMobile();
}
return $this->is_mobile;
}
/** Checks if visitor's device is a tablet device. */
public function isTablet(): bool
{
if ($this->is_tablet === null) {
$mobileDetect = $this->getMobileDetect();
$this->is_tablet = $mobileDetect->isTablet();
}
return $this->is_tablet;
}
/**
* @deprecated since 9.0.0 - This functionality was disabled. Function will be completely removed
* in the next major. There is no replacement, all clients should have the same experience.
*
* Sets mobile_device context variable.
*/
public function getMobileDevice(): bool
{
@trigger_error(
sprintf(
'%s is deprecated since version 9.0.0. There is no replacement.',
__METHOD__
),
E_USER_DEPRECATED
);
return false;
}
/** Returns mobile device type. */
public function getDevice(): int
{
static $device = null;
if ($device === null) {
if ($this->isTablet()) {
$device = Context::DEVICE_TABLET;
} elseif ($this->isMobile()) {
$device = Context::DEVICE_MOBILE;
} else {
$device = Context::DEVICE_COMPUTER;
}
}
return $device;
}
/**
* @return LocaleInterface|null
*/
public function getCurrentLocale()
{
return $this->currentLocale;
}
/**
* @deprecated since 9.0.0 - This functionality was disabled. Function will be completely removed
* in the next major. There is no replacement, all clients should have the same experience.
*
* Checks if mobile context is possible.
*
* @return bool
*/
protected function checkMobileContext()
{
@trigger_error(
sprintf(
'%s is deprecated since version 9.0.0. There is no replacement.',
__METHOD__
),
E_USER_DEPRECATED
);
return false;
}
/**
* Get a singleton instance of Context object.
*
* @return Context|null
*/
public static function getContext()
{
if (!isset(self::$instance)) {
self::$instance = new Context();
}
return self::$instance;
}
/**
* @param Context $testInstance Unit testing purpose only
*/
public static function setInstanceForTesting($testInstance)
{
self::$instance = $testInstance;
}
/**
* Unit testing purpose only.
*/
public static function deleteTestingInstance()
{
self::$instance = null;
}
/**
* Clone current context object.
*
* @return static
*/
public function cloneContext()
{
return clone $this;
}
/**
* Updates customer in the context, updates the cookie and writes the updated cookie.
*
* @param Customer $customer Created customer
*/
public function updateCustomer(Customer $customer)
{
// Update the customer in context object
$this->customer = $customer;
// Update basic information in the cookie
$this->cookie->id_customer = (int) $customer->id;
$this->cookie->customer_lastname = $customer->lastname;
$this->cookie->customer_firstname = $customer->firstname;
$this->cookie->passwd = $customer->passwd;
$this->cookie->logged = true;
$customer->logged = true;
$this->cookie->email = $customer->email;
// Don't confuse this with "id_guest" and Guest object, that's something completely different
$this->cookie->is_guest = $customer->isGuest();
/*
* If "re-display cart at login" option is enabled in Prestashop configuration,
* there is no cart in previous cookie or there is, but empty,
* and we managed to get that cart ID, we will re-use it.
*
* We don't want to flush his cart, if he made it when logged out.
*/
if (Configuration::get('PS_CART_FOLLOWING')
&& (empty($this->cookie->id_cart) || Cart::getNbProducts((int) $this->cookie->id_cart) == 0)
&& $idCart = (int) Cart::lastNoneOrderedCart($this->customer->id)
) {
$this->cart = new Cart($idCart);
$this->cart->secure_key = $customer->secure_key;
$this->cookie->id_guest = (int) $this->cart->id_guest;
/*
* Otherwise, normal cart recovery and update scenario.
*/
} else {
// Initialize new visit only if there is no visit identifier yet
if (!$this->cookie->id_guest) {
Guest::setNewGuest($this->cookie);
}
// If there is some cart created in the context before logging in
if (Validate::isLoadedObject($this->cart)) {
// We need to update the cart so it matches the customer
$this->cart->secure_key = $customer->secure_key;
$this->cart->id_guest = (int) $this->cookie->id_guest;
// Update and revalidate the selected delivery option
$idCarrier = (int) $this->cart->id_carrier;
$this->cart->id_carrier = 0;
if (!empty($idCarrier)) {
$deliveryOption = [$this->cart->id_address_delivery => $idCarrier . ','];
$this->cart->setDeliveryOption($deliveryOption);
} else {
$this->cart->setDeliveryOption(null);
}
// Set proper customer ID and assign addresses to the cart
$this->cart->id_customer = (int) $customer->id;
$this->cart->updateAddressId($this->cart->id_address_delivery, (int) Address::getFirstCustomerAddressId((int) $customer->id));
$this->cart->id_address_delivery = (int) Address::getFirstCustomerAddressId((int) $customer->id);
$this->cart->id_address_invoice = (int) Address::getFirstCustomerAddressId((int) $customer->id);
}
}
// If previous logic resolved to some cart to be used, save it and put this information to cookie
if (Validate::isLoadedObject($this->cart)) {
$this->cart->save();
$this->cookie->id_cart = (int) $this->cart->id;
}
// Physically save and send this cookie to the client
$this->cookie->write();
// Register new logged in session in customer_session table
$this->cookie->registerSession(new CustomerSession());
}
/**
* Returns a translator depending on service container availability and if the method
* is called by the installer or not.
*
* @param bool $isInstaller Set to true if the method is called by the installer
*
* @return Translator
*/
public function getTranslator($isInstaller = false)
{
if (null !== $this->translator && $this->language->locale === $this->translator->getLocale()) {
return $this->translator;
}
$sfContainer = SymfonyContainer::getInstance();
if ($isInstaller || null === $sfContainer) {
// symfony's container isn't available in front office, so we load and configure the translator component
$this->translator = $this->getTranslatorFromLocale($this->language->locale);
} else {
$this->translator = $sfContainer->get(TranslatorInterface::class);
// We need to set the locale here because in legacy BO pages, the translator is used
// before the TranslatorListener does its job of setting the locale according to the Request object
$this->translator->setLocale($this->language->locale);
}
return $this->translator;
}
/**
* Returns a new instance of Translator for the provided locale code.
*
* @param string $locale IETF language tag (eg. "en-US")
*
* @return Translator
*/
public function getTranslatorFromLocale($locale)
{
$cacheDir = _PS_CACHE_DIR_ . 'translations';
$translator = new Translator($locale, null, $cacheDir, false);
// In case we have at least 1 translated message, we return the current translator.
// If some translations are missing, clear cache
if (empty($locale) || count($translator->getCatalogue($locale)->all())) {
return $translator;
}
// However, in some case, even empty catalog were stored in the cache and then used as-is.
// For this one, we drop the cache and try to regenerate it.
if (is_dir($cacheDir)) {
$cache_file = Finder::create()
->files()
->in($cacheDir)
->depth('==0')
->name('*.' . $locale . '.*');
(new Filesystem())->remove($cache_file);
}
$translator->clearLanguage($locale);
$adminContext = defined('_PS_ADMIN_DIR_');
// Do not load DB translations when $this->language is InstallLanguage
// because it means that we're looking for the installer translations, so we're not yet connected to the DB
$withDB = !$this->language instanceof InstallLanguage;
$theme = $this->shop !== null ? $this->shop->theme : null;
if ($this instanceof Context) {
try {
$containerFinder = new ContainerFinder($this);
$container = $containerFinder->getContainer();
$translatorLoader = $container->get('prestashop.translation.translator_language_loader');
} catch (ContainerNotFoundException|ServiceNotFoundException $exception) {
$translatorLoader = null;
}
if (null === $translatorLoader) {
// If a container is still not found, instantiate manually the translator loader
// This will happen in the Front as we have legacy controllers, the Sf container won't be available.
// As we get the translator in the controller's constructor and the container is built in the init method, we won't find it here
$translatorLoader = (new TranslatorLanguageLoader(new ModuleRepository(_PS_ROOT_DIR_, _PS_MODULE_DIR_)));
}
$translatorLoader
->setIsAdminContext($adminContext)
->loadLanguage($translator, $locale, $withDB, $theme)
;
}
return $translator;
}
/**
* Returns directories that contain translation resources
*
* @return array
*/
protected function getTranslationResourcesDirectories()
{
// Default common translation folder
$locations = [_PS_ROOT_DIR_ . '/translations'];
// Translations for currently selected theme
if (null !== $this->shop) {
$activeThemeLocation = _PS_ROOT_DIR_ . '/themes/' . $this->shop->theme_name . '/translations';
if (is_dir($activeThemeLocation)) {
$locations[] = $activeThemeLocation;
}
}
return $locations;
}
/**
* Returns the computing precision according to the current currency.
* If previously requested, it will be stored in priceComputingPrecision property.
*
* @return int
*/
public function getComputingPrecision()
{
if ($this->priceComputingPrecision === null) {
$computingPrecision = new ComputingPrecision();
$this->priceComputingPrecision = $computingPrecision->getPrecision($this->currency->precision);
}
return $this->priceComputingPrecision;
}
}

592
classes/Cookie.php Normal file
View File

@@ -0,0 +1,592 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use Defuse\Crypto\Key;
use PrestaShop\PrestaShop\Core\Exception\CoreException;
use PrestaShop\PrestaShop\Core\Http\CookieOptions;
use PrestaShop\PrestaShop\Core\Session\SessionInterface;
/**
* @property bool $detect_language
* @property int $id_customer
* @property int $id_employee
* @property int $id_lang
* @property int $id_guest
* @property int|null $id_connections
* @property bool $is_guest
* @property bool $logged
* @property string $passwd
* @property int $session_id
* @property string $session_token
* @property string $shopContext
* @property int $last_activity
*/
class CookieCore
{
/**
* @deprecated since 9.0 use CookieOptions constants instead.
*/
public const SAMESITE_NONE = CookieOptions::SAMESITE_NONE;
/**
* @deprecated since 9.0 use CookieOptions constants instead.
*/
public const SAMESITE_LAX = CookieOptions::SAMESITE_LAX;
/**
* @deprecated since 9.0 use CookieOptions constants instead.
*/
public const SAMESITE_STRICT = CookieOptions::SAMESITE_STRICT;
/**
* @deprecated since 9.0 use CookieOptions constants instead.
*/
public const SAMESITE_AVAILABLE_VALUES = CookieOptions::SAMESITE_AVAILABLE_VALUES;
/** @var array Contain cookie content in a key => value format */
protected $_content = [];
/** @var string Crypted cookie name for setcookie() */
protected $_name;
/** @var int expiration date for setcookie() */
protected $_expire;
/** @var bool|string Website domain for setcookie() */
protected $_domain;
/** @var string|bool SameSite for setcookie() */
protected $_sameSite;
/** @var string Path for setcookie() */
protected $_path;
/** @var PhpEncryption cipher tool instance */
protected $cipherTool;
protected $_modified = false;
protected $_allow_writing;
protected $_salt;
protected $_standalone;
/** @var bool */
protected $_secure = false;
/** @var SessionInterface|null */
protected $session = null;
/**
* Get data if the cookie exists and else initialize an new one.
*
* @param string $name Cookie name before encrypting
* @param string $path Cookie path
* @param int|null $expire Cookie expiration time (default: 20 days from now)
* @param array|null $shared_urls Array of shared URLs for domain calculation
* @param bool $standalone Whether this is a standalone cookie (ie. the cookie is self-contained and not dependent on PrestaShop's context)
* @param bool $secure Whether the cookie should be secure (HTTPS only)
*/
public function __construct($name, $path = '', $expire = null, $shared_urls = null, $standalone = false, $secure = false)
{
$this->_content = [];
$this->_standalone = $standalone;
$this->_expire = null === $expire ? time() + 1728000 : (int) $expire;
$this->_path = trim(($this->_standalone ? '' : Context::getContext()->shop->physical_uri) . $path, '/\\') . '/';
if ($this->_path[0] != '/') {
$this->_path = '/' . $this->_path;
}
$this->_path = rawurlencode($this->_path);
$this->_path = str_replace(['%2F', '%7E', '%2B', '%26'], ['/', '~', '+', '&'], $this->_path);
$this->_domain = $this->getDomain($shared_urls);
$this->_sameSite = Configuration::get('PS_COOKIE_SAMESITE');
$this->_name = 'PrestaShop-' . md5(($this->_standalone ? '' : _PS_VERSION_) . $name . $this->_domain);
$this->_allow_writing = true;
$this->_salt = $this->_standalone ? str_pad('', 32, md5('ps' . __FILE__)) : _COOKIE_IV_;
if ($this->_standalone) {
$asciiSafeString = Defuse\Crypto\Encoding::saveBytesToChecksummedAsciiSafeString(Key::KEY_CURRENT_VERSION, str_pad($name, Key::KEY_BYTE_SIZE, md5(__FILE__)));
$this->cipherTool = new PhpEncryption($asciiSafeString);
} else {
$this->cipherTool = new PhpEncryption(_NEW_COOKIE_KEY_);
}
$this->_secure = (bool) $secure;
$this->update();
}
/**
* Disable cookie writing.
* Prevents the cookie from being written to the browser.
*/
public function disallowWriting()
{
$this->_allow_writing = false;
}
/**
* @param array|null $shared_urls
*
* @return bool|string
*/
protected function getDomain($shared_urls = null)
{
$httpHost = Tools::getHttpHost(false, false);
if (!$httpHost) {
return false;
}
$r = '!(?:(\w+)://)?(?:(\w+)\:(\w+)@)?([^/:]+)?(?:\:(\d*))?([^#?]+)?(?:\?([^#]+))?(?:#(.+$))?!i';
if (!preg_match($r, $httpHost, $out)) {
return false;
}
if (preg_match('/^(((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]{1}[0-9]|[1-9]).)' .
'{1}((25[0-5]|2[0-4][0-9]|[1]{1}[0-9]{2}|[1-9]{1}[0-9]|[0-9]).)' .
'{2}((25[0-5]|2[0-4][0-9]|[1]{1}[0-9]{2}|[1-9]{1}[0-9]|[0-9]){1}))$/', $out[4])) {
return false;
}
if (!strstr($httpHost, '.')) {
return false;
}
$domain = false;
if ($shared_urls !== null) {
foreach ($shared_urls as $shared_url) {
if ($shared_url != $out[4]) {
continue;
}
if (preg_match('/^(?:.*\.)?([^.]*(?:.{2,4})?\..{2,3})$/Ui', $shared_url, $res)) {
$domain = '.' . $res[1];
break;
}
}
}
if (!$domain) {
$domain = $out[4];
}
return $domain;
}
/**
* Set expiration date.
*
* @param int $expire Expiration time from now
*/
public function setExpire($expire)
{
$this->_expire = (int) $expire;
}
/**
* Magic method wich return cookie data from _content array.
*
* @param string $key key wanted
*
* @return string value corresponding to the key
*/
public function __get($key)
{
return isset($this->_content[$key]) ? $this->_content[$key] : false;
}
/**
* Magic method which check if key exists in the cookie.
*
* @param string $key key wanted
*
* @return bool key existence
*/
public function __isset($key)
{
return isset($this->_content[$key]);
}
/**
* Magic method which adds data into _content array.
*
* @param string $key Access key for the value
* @param mixed $value Value corresponding to the key
*
* @throws Exception
*/
public function __set($key, $value)
{
if (is_array($value)) {
throw new PrestaShopException('Cookie value can\'t be an array.');
}
if (preg_match('/¤|\|/', $key . $value)) {
throw new PrestaShopException('Forbidden chars in cookie');
}
if (!$this->_modified && (!array_key_exists($key, $this->_content) || $this->_content[$key] != $value)) {
$this->_modified = true;
}
$this->_content[$key] = $value;
}
/**
* Magic method which delete data into _content array.
*
* @param string $key key wanted
*/
public function __unset($key)
{
if (isset($this->_content[$key])) {
$this->_modified = true;
}
unset($this->_content[$key]);
}
/**
* Delete cookie
* As of version 1.5 don't call this function, use Customer::logout() or Employee::logout() instead;.
*/
public function logout()
{
$this->deleteSession();
$this->_content = [];
$this->encryptAndSetCookie();
unset($_COOKIE[$this->_name]);
$this->_modified = true;
}
/**
* Soft logout, delete everything linked to the customer
* but leave their affiliate's informations intact.
* As of version 1.5 don't call this function, use Customer::mylogout() instead;.
*/
public function mylogout()
{
$this->deleteSession();
unset(
$this->_content['id_customer'],
$this->_content['id_guest'],
$this->_content['is_guest'],
$this->_content['id_connections'],
$this->_content['customer_lastname'],
$this->_content['customer_firstname'],
$this->_content['passwd'],
$this->_content['logged'],
$this->_content['email'],
$this->_content['id_cart'],
$this->_content['id_address_invoice'],
$this->_content['id_address_delivery']
);
$this->_modified = true;
}
/**
* Create a new guest log entry.
* Removes current customer and guest IDs and creates a new guest session.
*/
public function makeNewLog()
{
unset(
$this->_content['id_customer'],
$this->_content['id_guest']
);
Guest::setNewGuest($this);
$this->_modified = true;
}
/**
* Get cookie content and update internal data.
* Decrypts and validates the cookie content, handles checksum verification.
*
* @param bool $nullValues Whether to handle null values
*/
public function update($nullValues = false)
{
if (isset($_COOKIE[$this->_name])) {
/* Decrypt cookie content */
$content = $this->cipherTool->decrypt($_COOKIE[$this->_name]);
// printf("\$content = %s<br />", $content);
/* Get cookie checksum */
$tmpTab = explode('¤', $content);
// remove the checksum which is the last element
array_pop($tmpTab);
$content_for_checksum = implode('¤', $tmpTab) . '¤';
$checksum = hash('sha256', $this->_salt . $content_for_checksum);
// printf("\$checksum = %s<br />", $checksum);
/* Unserialize cookie content */
$tmpTab = explode('¤', $content);
foreach ($tmpTab as $keyAndValue) {
$tmpTab2 = explode('|', $keyAndValue);
if (count($tmpTab2) == 2) {
$this->_content[$tmpTab2[0]] = $tmpTab2[1];
}
}
/* Check if cookie has not been modified */
if (!isset($this->_content['checksum']) || $this->_content['checksum'] != $checksum) {
$this->logout();
}
if (!isset($this->_content['date_add'])) {
$this->_content['date_add'] = date('Y-m-d H:i:s');
}
} else {
$this->_content['date_add'] = date('Y-m-d H:i:s');
}
// checks if the language exists, if not choose the default language
if (!$this->_standalone && !Language::getLanguage((int) $this->id_lang)) {
$this->id_lang = (int) Configuration::get('PS_LANG_DEFAULT');
// set detect_language to force going through Tools::setCookieLanguage to figure out browser lang
$this->detect_language = true;
}
}
/**
* Encrypt and set the Cookie.
*
* @param string|null $cookie Cookie content
*
* @return bool Indicates whether the Cookie was successfully set
*/
protected function encryptAndSetCookie($cookie = null)
{
if ($cookie) {
$content = $this->cipherTool->encrypt($cookie);
$time = $this->_expire;
} else {
$content = 0;
$time = 1;
}
/*
* We need to check if the new cookie will be compliant with RFC 2965, maximum of 4096 bytes
* per cookie. Major browsers follow this very closely and will refuse to save this cookie.
*
* If we exceed this value, some module is saving something to cookie that it shouldn't save,
* and overflowing the cookie. It's absolutely critical that this does not happen because
* it breaks for example all cart functionality.
*
* We are using strlen because it calculates the byte count, we don't care about character
* count in case of multi-byte characters.
*/
if (strlen($this->_name . $content) > 4096) {
throw new PrestaShopException('Error during setting a cookie. Combined size of name and value cannot exceed 4096 characters. Larger cookie is not compliant with RFC 2965 and will not be accepted by the browser.');
}
return setcookie(
$this->_name,
$content,
[
'expires' => $time,
'path' => $this->_path,
'domain' => (string) $this->_domain,
'secure' => $this->_secure,
'httponly' => true,
'samesite' => in_array((string) $this->_sameSite, CookieOptions::SAMESITE_AVAILABLE_VALUES) ? (string) $this->_sameSite : CookieOptions::SAMESITE_NONE,
]
);
}
/**
* Destructor.
* Automatically saves the cookie when the object is destroyed.
*/
public function __destruct()
{
$this->write();
}
/**
* Save cookie with setcookie().
*/
public function write()
{
if (!$this->_modified || headers_sent() || !$this->_allow_writing) {
return;
}
$previousChecksum = $cookie = '';
/* Serialize cookie content */
if (isset($this->_content['checksum'])) {
$previousChecksum = $this->_content['checksum'];
unset($this->_content['checksum']);
}
foreach ($this->_content as $key => $value) {
$cookie .= $key . '|' . $value . '¤';
}
/* Add checksum to cookie */
$newChecksum = hash('sha256', $this->_salt . $cookie);
// do not set cookie if the checksum is the same: it means the content has not changed!
if ($previousChecksum === $newChecksum) {
return;
}
$cookie .= 'checksum|' . $newChecksum;
$this->_modified = false;
/* Cookies are encrypted for evident security reasons */
return $this->encryptAndSetCookie($cookie);
}
/**
* Get a family of variables with a common prefix (e.g. "filter_").
*
* @param string $origin The prefix to search for
*
* @return array Array of key-value pairs matching the prefix
*/
public function getFamily($origin)
{
$result = [];
if (count($this->_content) == 0) {
return $result;
}
foreach ($this->_content as $key => $value) {
if (strncmp($key, $origin, strlen($origin)) == 0) {
$result[$key] = $value;
}
}
return $result;
}
/**
* Remove a family of variables with a common prefix.
*
* @param string $origin The prefix of variables to remove
*/
public function unsetFamily($origin)
{
$family = $this->getFamily($origin);
foreach (array_keys($family) as $member) {
unset($this->$member);
}
}
/**
* Get all cookie content.
*
* @return array All cookie data as key-value pairs
*/
public function getAll()
{
return $this->_content;
}
/**
* @return string name of cookie
*/
public function getName()
{
return $this->_name;
}
/**
* Check if the cookie exists.
*
* @return bool
*/
public function exists()
{
return isset($_COOKIE[$this->_name]);
}
/**
* Register a new session for the current user.
*
* @param SessionInterface $session The session object to register
*
* @throws CoreException If no valid user ID is found
*/
public function registerSession(SessionInterface $session)
{
if (isset($this->id_employee)) {
$session->setUserId((int) $this->id_employee);
} elseif (isset($this->id_customer)) {
$session->setUserId((int) $this->id_customer);
} else {
throw new CoreException('Invalid user id');
}
$session->setToken(sha1(time() . uniqid()));
$session->add();
$this->session_id = $session->getId();
$this->session_token = $session->getToken();
}
/**
* Delete the current session.
* Removes the session if it exists.
*
* @return bool True if session was deleted, false if no session exists
*/
public function deleteSession()
{
if (!isset($this->session_id)) {
return false;
}
$session = $this->getSession($this->session_id);
if ($session !== null) {
$session->delete();
return true;
}
return false;
}
/**
* Check if the current session is still alive and valid.
* Verifies session ID, token, and user ID match.
*
* @return bool True if session is valid and alive
*/
public function isSessionAlive()
{
if (!isset($this->session_id) || !isset($this->session_token)) {
return false;
}
$session = $this->getSession($this->session_id);
return
$session !== null
&& $session->getToken() === $this->session_token
&& (
(int) $this->id_employee === $session->getUserId()
|| (int) $this->id_customer === $session->getUserId()
)
;
}
/**
* Retrieve session based on a session ID and the employee or customer ID
* Creates appropriate session object (Employee or Customer) and updates its timestamp.
*
* @param int $sessionId The session ID to retrieve
*
* @return SessionInterface|null The session object or null if not found
*/
public function getSession($sessionId)
{
if ($this->session !== null) {
return $this->session;
}
if (isset($this->id_employee)) {
$this->session = new EmployeeSession($sessionId);
} elseif (isset($this->id_customer)) {
$this->session = new CustomerSession($sessionId);
}
if (isset($this->session) && Validate::isLoadedObject($this->session)) {
// Update session date_upd
$this->session->save();
}
return $this->session;
}
}

509
classes/Country.php Normal file
View File

@@ -0,0 +1,509 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class CountryCore.
*/
class CountryCore extends ObjectModel
{
/** @var int */
public $id;
/** @var int Zone id which country belongs */
public $id_zone;
/** @var int Currency id which country belongs */
public $id_currency;
/** @var string 2 letters iso code */
public $iso_code;
/** @var int international call prefix */
public $call_prefix;
/** @var string[]|string Name */
public $name;
/** @var bool Contain states */
public $contains_states;
/** @var bool Need identification number dni/nif/nie */
public $need_identification_number;
/** @var bool Need Zip Code */
public $need_zip_code;
/** @var string Zip Code Format */
public $zip_code_format;
/** @var bool Display or not the tax incl./tax excl. mention in the front office */
public $display_tax_label = true;
/** @var bool Status for delivery */
public $active = true;
protected static $_idZones = [];
public const GEOLOC_ALLOWED = 0;
public const GEOLOC_CATALOG_MODE = 1;
public const GEOLOC_FORBIDDEN = 2;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'country',
'primary' => 'id_country',
'multilang' => true,
'fields' => [
'id_zone' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_currency' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'call_prefix' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
'iso_code' => ['type' => self::TYPE_STRING, 'validate' => 'isLanguageIsoCode', 'required' => true, 'size' => 3],
'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'contains_states' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'required' => true],
'need_identification_number' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'required' => true],
'need_zip_code' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'zip_code_format' => ['type' => self::TYPE_STRING, 'validate' => 'isZipCodeFormat', 'size' => 12],
'display_tax_label' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'required' => true],
/* Lang fields */
'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 64],
],
'associations' => [
'zone' => ['type' => self::HAS_ONE],
'currency' => ['type' => self::HAS_ONE],
],
];
protected static $cache_iso_by_id = [];
protected $webserviceParameters = [
'objectsNodeName' => 'countries',
'fields' => [
'id_zone' => ['xlink_resource' => 'zones'],
'id_currency' => ['xlink_resource' => 'currencies'],
],
];
/**
* Deletes current Country from the database.
*
* @return bool True if delete was successful
*
* @throws PrestaShopException
*/
public function delete()
{
if ((int) $this->id === (int) Configuration::get('PS_COUNTRY_DEFAULT')) {
throw new PrestaShopException(sprintf('Default country "%s" cannot be deleted.', $this->iso_code));
}
if (!parent::delete()) {
return false;
}
return Db::getInstance()->execute('DELETE FROM ' . _DB_PREFIX_ . 'cart_rule_country WHERE id_country = ' . (int) $this->id);
}
/**
* @brief Return available countries
*
* @param int $idLang Language ID
* @param bool $active return only active coutries
* @param bool $containStates return only country with states
* @param bool $listStates Include the states list with the returned list
*
* @return array Countries and corresponding zones
*/
public static function getCountries($idLang, $active = false, $containStates = false, $listStates = true)
{
$countries = [];
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT cl.*,c.*, cl.`name` country, z.`name` zone
FROM `' . _DB_PREFIX_ . 'country` c ' . Shop::addSqlAssociation('country', 'c') . '
LEFT JOIN `' . _DB_PREFIX_ . 'country_lang` cl ON (c.`id_country` = cl.`id_country` AND cl.`id_lang` = ' . (int) $idLang . ')
LEFT JOIN `' . _DB_PREFIX_ . 'zone` z ON (z.`id_zone` = c.`id_zone`)
WHERE 1' . ($active ? ' AND c.active = 1' : '') . ($containStates ? ' AND c.`contains_states` = ' . (int) $containStates : '') . '
ORDER BY cl.name ASC');
foreach ($result as $row) {
$countries[$row['id_country']] = $row;
}
if ($listStates) {
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT * FROM `' . _DB_PREFIX_ . 'state` ORDER BY `name` ASC');
foreach ($result as $row) {
if (isset($countries[$row['id_country']]) && $row['active'] == 1) { /* Does not keep the state if its country has been disabled and not selected */
$countries[$row['id_country']]['states'][] = $row;
}
}
}
return $countries;
}
public static function getCountriesByIdShop($idShop, $idLang)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT *
FROM `' . _DB_PREFIX_ . 'country` c
LEFT JOIN `' . _DB_PREFIX_ . 'country_shop` cs ON (cs.`id_country`= c.`id_country`)
LEFT JOIN `' . _DB_PREFIX_ . 'country_lang` cl ON (c.`id_country` = cl.`id_country` AND cl.`id_lang` = ' . (int) $idLang . ')
WHERE `id_shop` = ' . (int) $idShop);
}
/**
* Get a country ID with its iso code.
*
* @param string $isoCode Country iso code
* @param bool $active return only active countries
*
* @return int|bool Country ID
*/
public static function getByIso($isoCode, $active = false)
{
if (!Validate::isLanguageIsoCode($isoCode)) {
throw new PrestaShopException('Given iso code (' . $isoCode . ') is not valid.');
}
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow(
'
SELECT `id_country`
FROM `' . _DB_PREFIX_ . 'country`
WHERE `iso_code` = \'' . pSQL(strtoupper($isoCode)) . '\''
. ($active ? ' AND active = 1' : '')
);
if (isset($result['id_country'])) {
return (int) $result['id_country'];
}
return false;
}
/**
* Get Zone ID by Country.
*
* @param int $idCountry Country ID
*
* @return bool|int
*/
public static function getIdZone($idCountry)
{
if (!Validate::isUnsignedId($idCountry)) {
throw new PrestaShopException('Country ID is invalid.');
}
if (isset(self::$_idZones[$idCountry])) {
return (int) self::$_idZones[$idCountry];
}
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
SELECT `id_zone`
FROM `' . _DB_PREFIX_ . 'country`
WHERE `id_country` = ' . (int) $idCountry);
if (isset($result['id_zone'])) {
self::$_idZones[$idCountry] = (int) $result['id_zone'];
return (int) $result['id_zone'];
}
return false;
}
/**
* Get a country name with its ID.
*
* @param int $idLang Language ID
* @param int $idCountry Country ID
*
* @return string Country name
*/
public static function getNameById($idLang, $idCountry)
{
$key = 'country_getNameById_' . $idCountry . '_' . $idLang;
if (!Cache::isStored($key)) {
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
'
SELECT `name`
FROM `' . _DB_PREFIX_ . 'country_lang`
WHERE `id_lang` = ' . (int) $idLang . '
AND `id_country` = ' . (int) $idCountry
);
Cache::store($key, $result);
return $result;
}
return Cache::retrieve($key);
}
/**
* Get a country iso with its ID.
*
* @param int $idCountry Country ID
*
* @return string|bool Country iso
*/
public static function getIsoById($idCountry)
{
if (!isset(Country::$cache_iso_by_id[$idCountry])) {
Country::$cache_iso_by_id[$idCountry] = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
SELECT `iso_code`
FROM `' . _DB_PREFIX_ . 'country`
WHERE `id_country` = ' . (int) $idCountry);
}
if (isset(Country::$cache_iso_by_id[$idCountry])) {
return Country::$cache_iso_by_id[$idCountry];
}
return false;
}
/**
* Get a country id with its name.
*
* @param int|null $idLang Language ID
* @param string $country Country Name
*
* @return int|bool Country ID
*/
public static function getIdByName($idLang, $country)
{
$sql = '
SELECT `id_country`
FROM `' . _DB_PREFIX_ . 'country_lang`
WHERE `name` = \'' . pSQL($country) . '\'';
if ($idLang) {
$sql .= ' AND `id_lang` = ' . (int) $idLang;
}
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql);
if (isset($result['id_country'])) {
return (int) $result['id_country'];
}
return false;
}
/**
* Does the Country need a zip code?
*
* @param int $idCountry Country ID
*
* @return bool Indicates whether the Country needs a zip code
*/
public static function getNeedZipCode($idCountry)
{
if (!(int) $idCountry) {
return false;
}
return (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
SELECT `need_zip_code`
FROM `' . _DB_PREFIX_ . 'country`
WHERE `id_country` = ' . (int) $idCountry);
}
/**
* Get zip code format for Country.
*
* @param int $idCountry Country ID
*
* @return bool|false|string|null
*/
public static function getZipCodeFormat($idCountry)
{
if (!(int) $idCountry) {
return false;
}
$zipCodeFormat = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
SELECT `zip_code_format`
FROM `' . _DB_PREFIX_ . 'country`
WHERE `id_country` = ' . (int) $idCountry);
if ($zipCodeFormat) {
return $zipCodeFormat;
}
return false;
}
/**
* Get Countries by Zone ID.
*
* @param int $idZone Zone ID
* @param int $idLang Language ID
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public static function getCountriesByZoneId($idZone, $idLang)
{
if (empty($idZone)) {
throw new PrestaShopException('Zone ID is invalid.');
}
if (empty($idLang)) {
throw new PrestaShopException('Lang ID is invalid.');
}
$sql = ' SELECT DISTINCT c.*, cl.*
FROM `' . _DB_PREFIX_ . 'country` c
' . Shop::addSqlAssociation('country', 'c', false) . '
LEFT JOIN `' . _DB_PREFIX_ . 'state` s ON (s.`id_country` = c.`id_country`)
LEFT JOIN `' . _DB_PREFIX_ . 'country_lang` cl ON (c.`id_country` = cl.`id_country`)
WHERE (c.`id_zone` = ' . (int) $idZone . ' OR s.`id_zone` = ' . (int) $idZone . ')
AND `id_lang` = ' . (int) $idLang;
return Db::getInstance()->executeS($sql);
}
/**
* Does the Country need a DNI.
*
* @return bool Indicates whether the Country needs a DNI
*/
public function isNeedDni()
{
return Country::isNeedDniByCountryId($this->id);
}
/**
* Does the given Country need a DNI?
*
* @param int $idCountry Country ID
*
* @return bool Indicates whether the Country needs a DNI
*/
public static function isNeedDniByCountryId($idCountry)
{
return (bool) Db::getInstance()->getValue('
SELECT `need_identification_number`
FROM `' . _DB_PREFIX_ . 'country`
WHERE `id_country` = ' . (int) $idCountry);
}
/**
* Does the given Country contain States?
*
* @param int $idCountry Country ID
*
* @return bool Indicates whether the Country contains States
*/
public static function containsStates($idCountry)
{
return (bool) Db::getInstance()->getValue('
SELECT `contains_states`
FROM `' . _DB_PREFIX_ . 'country`
WHERE `id_country` = ' . (int) $idCountry);
}
/**
* Apply Zone to selected Countries.
*
* @param array $idsCountries Country array
* @param int $idZone Zone ID
*
* @return bool Indicates whether the Zone was successfully applied
*/
public function affectZoneToSelection($idsCountries, $idZone)
{
// cast every array values to int (security)
$idsCountries = array_map('intval', $idsCountries);
return Db::getInstance()->execute('
UPDATE `' . _DB_PREFIX_ . 'country` SET `id_zone` = ' . (int) $idZone . ' WHERE `id_country` IN (' . implode(',', $idsCountries) . ')
');
}
/**
* Replace letters of zip code format And check this format on the zip code.
*
* @param string $zipCode zip code
*
* @return bool Indicates whether the zip code is correct
*/
public function checkZipCode($zipCode)
{
if (empty($this->zip_code_format)) {
return true;
}
$zipRegexp = '/^' . $this->zip_code_format . '$/ui';
$zipRegexp = str_replace('N', '[0-9]', $zipRegexp);
$zipRegexp = str_replace('L', '[a-zA-Z]', $zipRegexp);
$zipRegexp = str_replace('C', $this->iso_code, $zipRegexp);
return (bool) preg_match($zipRegexp, $zipCode);
}
/**
* Add module restrictions.
*
* @param array $shops Shops array
* @param array $countries Countries array
* @param array $modules Modules array
*
* @return bool Indictes whether the restrictions were successfully applied
*/
public static function addModuleRestrictions(array $shops = [], array $countries = [], array $modules = [])
{
if (!count($shops)) {
$shops = Shop::getShops(true, null, true);
}
if (!count($countries)) {
if (null !== Context::getContext()->cookie) {
$id_lang = (int) Context::getContext()->cookie->id_lang;
} else {
$id_lang = (int) Context::getContext()->language->id;
}
$countries = Country::getCountries($id_lang);
}
if (!count($modules)) {
$modules = Module::getPaymentModules();
}
$sql = false;
foreach ($shops as $idShop) {
foreach ($countries as $country) {
foreach ($modules as $module) {
$sql .= '(' . (int) $module['id_module'] . ', ' . (int) $idShop . ', ' . (int) $country['id_country'] . '),';
}
}
}
if ($sql) {
$sql = 'INSERT IGNORE INTO `' . _DB_PREFIX_ . 'module_country` (`id_module`, `id_shop`, `id_country`) VALUES ' . rtrim($sql, ',');
return Db::getInstance()->execute($sql);
} else {
return true;
}
}
/**
* Adds current Country as a new Object to the database.
*
* @param bool $autoDate Automatically set `date_upd` and `date_add` columns
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Indicates whether the Country has been successfully added
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function add($autoDate = true, $nullValues = false)
{
$return = parent::add($autoDate, $nullValues) && self::addModuleRestrictions([], [['id_country' => $this->id]], []);
return $return;
}
}

1175
classes/Currency.php Normal file

File diff suppressed because it is too large Load Diff

97
classes/Curve.php Normal file
View File

@@ -0,0 +1,97 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Data structure to store curves
*/
class CurveCore
{
/**
* @var float[] indexed by string
*/
protected $values = [];
/**
* @var string
*/
protected $label;
/**
* Can be: bars, steps
*
* @var string
*/
protected $type;
/**
* @param array $values
*/
public function setValues($values)
{
$this->values = $values;
}
/**
* @param bool $time_mode
*
* @return string
*/
public function getValues($time_mode = false)
{
ksort($this->values);
$string = '';
foreach ($this->values as $key => $value) {
$string .= '[' . addslashes((string) $key) . ($time_mode ? '000' : '') . ',' . (float) $value . '],';
}
return '{data:[' . rtrim($string, ',') . ']'
. (!empty($this->label) ? ',label:"' . $this->label . '"' : '') . ''
. (!empty($this->type) ? ',' . $this->type : '') . '}';
}
/**
* @param string $x
* @param float $y
*/
public function setPoint($x, $y)
{
$this->values[(string) $x] = (float) $y;
}
/**
* @param string $label
*/
public function setLabel($label)
{
$this->label = $label;
}
/**
* @param string $type accepts only 'bars' or 'steps'
*/
public function setType($type)
{
$this->type = '';
if ($type == 'bars') {
$this->type = 'bars:{show:true,lineWidth:10}';
}
if ($type == 'steps') {
$this->type = 'lines:{show:true,steps:true}';
}
}
/**
* @param string $x
*
* @return float|null return point if found, null else
*/
public function getPoint($x)
{
if (array_key_exists((string) $x, $this->values)) {
return $this->values[(string) $x];
}
return null;
}
}

1570
classes/Customer.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class CustomerAddressCore.
*
* Holds address info of a Customer.
* This class extends AddressCore to be differentiated from other AddressCore objects in DB.
*/
class CustomerAddressCore extends Address
{
}

177
classes/CustomerMessage.php Normal file
View File

@@ -0,0 +1,177 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShopBundle\Form\Admin\Type\FormattedTextareaType;
/**
* Class CustomerMessageCore.
*/
class CustomerMessageCore extends ObjectModel
{
public $id;
/** @var int CustomerThread ID */
public $id_customer_thread;
/** @var int */
public $id_employee;
/** @var int */
public $id_product;
/** @var string */
public $message;
/** @var string */
public $file_name;
/** @var string */
public $ip_address;
/** @var string */
public $user_agent;
/** @var bool */
public $private;
/** @var string */
public $date_add;
/** @var string */
public $date_upd;
/** @var bool */
public $read;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'customer_message',
'primary' => 'id_customer_message',
'fields' => [
'id_employee' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_product' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_customer_thread' => ['type' => self::TYPE_INT],
'ip_address' => ['type' => self::TYPE_STRING, 'validate' => 'isIp2Long', 'size' => 16],
'message' => ['type' => self::TYPE_HTML, 'required' => true, 'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4, 'validate' => 'isCleanHtml'],
'file_name' => ['type' => self::TYPE_STRING, 'size' => 18],
'user_agent' => ['type' => self::TYPE_STRING, 'size' => 255],
'private' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'read' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
],
];
/** @var array */
protected $webserviceParameters = [
'fields' => [
'id_employee' => [
'xlink_resource' => 'employees',
],
'id_product' => [
'xlink_resource' => 'products',
],
'id_customer_thread' => [
'xlink_resource' => 'customer_threads',
],
],
];
/**
* Get CustomerMessages by Order ID.
*
* @param int $idOrder Order ID
* @param bool $private Private
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public static function getMessagesByOrderId($idOrder, $private = true)
{
return Db::getInstance()->executeS('
SELECT cm.*,
c.`firstname` AS cfirstname,
c.`lastname` AS clastname,
e.`firstname` AS efirstname,
e.`lastname` AS elastname,
(COUNT(cm.id_customer_message) = 0 AND ct.id_customer != 0) AS is_new_for_me
FROM `' . _DB_PREFIX_ . 'customer_message` cm
LEFT JOIN `' . _DB_PREFIX_ . 'customer_thread` ct
ON ct.`id_customer_thread` = cm.`id_customer_thread`
LEFT JOIN `' . _DB_PREFIX_ . 'customer` c
ON ct.`id_customer` = c.`id_customer`
LEFT OUTER JOIN `' . _DB_PREFIX_ . 'employee` e
ON e.`id_employee` = cm.`id_employee`
WHERE ct.id_order = ' . (int) $idOrder . '
' . (!$private ? 'AND cm.`private` = 0' : '') . '
GROUP BY cm.id_customer_message
ORDER BY cm.date_add DESC
');
}
/**
* Get total CustomerMessages.
*
* @param string|null $where Additional SQL query
*
* @return int Amount of CustomerMessages found
*/
public static function getTotalCustomerMessages($where = null)
{
if (null === $where) {
return (int) Db::getInstance()->getValue(
'
SELECT COUNT(*)
FROM ' . _DB_PREFIX_ . 'customer_message
LEFT JOIN `' . _DB_PREFIX_ . 'customer_thread` ct ON (cm.`id_customer_thread` = ct.`id_customer_thread`)
WHERE 1' . Shop::addSqlRestriction()
);
} else {
return (int) Db::getInstance()->getValue(
'
SELECT COUNT(*)
FROM ' . _DB_PREFIX_ . 'customer_message cm
LEFT JOIN `' . _DB_PREFIX_ . 'customer_thread` ct ON (cm.`id_customer_thread` = ct.`id_customer_thread`)
WHERE ' . $where . Shop::addSqlRestriction()
);
}
}
/**
* Deletes current CustomerMessage from the database.
*
* @return bool `true` if delete was successful
*
* @throws PrestaShopException
*/
public function delete()
{
if (!empty($this->file_name)) {
@unlink(_PS_UPLOAD_DIR_ . basename($this->file_name));
}
return parent::delete();
}
/**
* Get the last message for a thread customer.
*
* @param int $id_customer_thread Thread customer reference
*
* @return string Last message
*/
public static function getLastMessageForCustomerThread($id_customer_thread)
{
return (string) Db::getInstance()->getValue(
'
SELECT message
FROM ' . _DB_PREFIX_ . 'customer_message
WHERE id_customer_thread = ' . (int) $id_customer_thread . '
ORDER BY date_add DESC'
);
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Core\Session\SessionInterface;
class CustomerSessionCore extends ObjectModel implements SessionInterface
{
public $id;
/** @var int Id Customer */
public $id_customer;
/** @var string Token */
public $token;
/** @var string Object last modification date */
public $date_upd;
/** @var string Object creation date */
public $date_add;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'customer_session',
'primary' => 'id_customer_session',
'fields' => [
'id_customer' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'token' => ['type' => self::TYPE_STRING, 'validate' => 'isSha1', 'size' => 40, 'copy_post' => false],
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate', 'copy_post' => false],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate', 'copy_post' => false],
],
];
/**
* {@inheritdoc}
*/
public function getId()
{
return $this->id;
}
/**
* {@inheritdoc}
*/
public function setUserId($idCustomer)
{
$this->id_customer = (int) $idCustomer;
}
/**
* {@inheritdoc}
*/
public function getUserId()
{
return (int) $this->id_customer;
}
/**
* {@inheritdoc}
*/
public function setToken($token)
{
$this->token = (string) $token;
}
/**
* {@inheritdoc}
*/
public function getToken()
{
return $this->token;
}
}

274
classes/CustomerThread.php Normal file
View File

@@ -0,0 +1,274 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
class CustomerThreadCore extends ObjectModel
{
public $id;
public $id_shop;
public $id_lang;
public $id_contact;
public $id_customer;
public $id_order;
public $id_product;
public $status;
public $email;
public $token;
public $date_add;
public $date_upd;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'customer_thread',
'primary' => 'id_customer_thread',
'fields' => [
'id_lang' => [
'type' => self::TYPE_INT,
'validate' => 'isUnsignedId',
'required' => true,
],
'id_contact' => [
'type' => self::TYPE_INT,
'validate' => 'isUnsignedId',
'required' => true,
],
'id_shop' => [
'type' => self::TYPE_INT,
'validate' => 'isUnsignedId',
],
'id_customer' => [
'type' => self::TYPE_INT,
'validate' => 'isUnsignedId',
],
'id_order' => [
'type' => self::TYPE_INT,
'validate' => 'isUnsignedId',
],
'id_product' => [
'type' => self::TYPE_INT,
'validate' => 'isUnsignedId',
],
'email' => [
'type' => self::TYPE_STRING,
'validate' => 'isEmail',
'size' => 255,
],
'token' => [
'type' => self::TYPE_STRING,
'validate' => 'isGenericName',
'required' => true,
'size' => 12,
],
'status' => [
'type' => self::TYPE_STRING,
],
'date_add' => [
'type' => self::TYPE_DATE,
'validate' => 'isDate',
],
'date_upd' => [
'type' => self::TYPE_DATE,
'validate' => 'isDate',
],
],
];
protected $webserviceParameters = [
'fields' => [
'id_lang' => [
'xlink_resource' => 'languages',
],
'id_shop' => [
'xlink_resource' => 'shops',
],
'id_customer' => [
'xlink_resource' => 'customers',
],
'id_order' => [
'xlink_resource' => 'orders',
],
'id_product' => [
'xlink_resource' => 'products',
],
],
'associations' => [
'customer_messages' => [
'resource' => 'customer_message',
'id' => [
'required' => true,
],
],
],
];
public function getWsCustomerMessages()
{
return Db::getInstance()->executeS('
SELECT `id_customer_message` id
FROM `' . _DB_PREFIX_ . 'customer_message`
WHERE `id_customer_thread` = ' . (int) $this->id);
}
public function delete()
{
if (!Validate::isUnsignedId($this->id)) {
return false;
}
$return = true;
$result = Db::getInstance()->executeS(
'
SELECT `id_customer_message`
FROM `' . _DB_PREFIX_ . 'customer_message`
WHERE `id_customer_thread` = ' . (int) $this->id
);
if (count($result)) {
foreach ($result as $res) {
$message = new CustomerMessage((int) $res['id_customer_message']);
if (!Validate::isLoadedObject($message)) {
$return = false;
} else {
$return = $return && $message->delete();
}
}
}
return $return && parent::delete();
}
public static function getCustomerMessages($id_customer, $read = null, $id_order = null)
{
$sql = 'SELECT *
FROM ' . _DB_PREFIX_ . 'customer_thread ct
LEFT JOIN ' . _DB_PREFIX_ . 'customer_message cm
ON ct.id_customer_thread = cm.id_customer_thread
WHERE id_customer = ' . (int) $id_customer;
if ($read !== null) {
$sql .= ' AND cm.`read` = ' . (int) $read;
}
if ($id_order !== null) {
$sql .= ' AND ct.`id_order` = ' . (int) $id_order;
}
return Db::getInstance()->executeS($sql);
}
public static function getIdCustomerThreadByEmailAndIdOrder($email, $id_order)
{
return Db::getInstance()->getValue(
'
SELECT cm.id_customer_thread
FROM ' . _DB_PREFIX_ . 'customer_thread cm
WHERE cm.email = \'' . pSQL($email) . '\'
AND cm.id_shop = ' . (int) Context::getContext()->shop->id . '
AND cm.id_order = ' . (int) $id_order
);
}
public static function getContacts()
{
return Db::getInstance()->executeS('
SELECT cl.*, COUNT(*) as total, (
SELECT id_customer_thread
FROM ' . _DB_PREFIX_ . 'customer_thread ct2
WHERE status = "open" AND ct.id_contact = ct2.id_contact
' . Shop::addSqlRestriction() . '
ORDER BY date_upd ASC
LIMIT 1
) as id_customer_thread
FROM ' . _DB_PREFIX_ . 'customer_thread ct
LEFT JOIN ' . _DB_PREFIX_ . 'contact_lang cl
ON (cl.id_contact = ct.id_contact AND cl.id_lang = ' . (int) Context::getContext()->language->id . ')
WHERE ct.status = "open"
AND ct.id_contact IS NOT NULL
AND cl.id_contact IS NOT NULL
' . Shop::addSqlRestriction() . '
GROUP BY ct.id_contact HAVING COUNT(*) > 0
');
}
public static function getTotalCustomerThreads($where = null)
{
if (null === $where) {
return (int) Db::getInstance()->getValue(
'
SELECT COUNT(*)
FROM ' . _DB_PREFIX_ . 'customer_thread
WHERE 1 ' . Shop::addSqlRestriction()
);
} else {
return (int) Db::getInstance()->getValue(
'
SELECT COUNT(*)
FROM ' . _DB_PREFIX_ . 'customer_thread
WHERE ' . $where . Shop::addSqlRestriction()
);
}
}
public static function getMessageCustomerThreads($id_customer_thread)
{
return Db::getInstance()->executeS('
SELECT ct.*, cm.*, cl.name subject, CONCAT(e.firstname, \' \', e.lastname) employee_name,
CONCAT(c.firstname, \' \', c.lastname) customer_name, c.firstname
FROM ' . _DB_PREFIX_ . 'customer_thread ct
LEFT JOIN ' . _DB_PREFIX_ . 'customer_message cm
ON (ct.id_customer_thread = cm.id_customer_thread)
LEFT JOIN ' . _DB_PREFIX_ . 'contact_lang cl
ON (cl.id_contact = ct.id_contact AND cl.id_lang = ' . (int) Context::getContext()->language->id . ')
LEFT JOIN ' . _DB_PREFIX_ . 'employee e
ON e.id_employee = cm.id_employee
LEFT JOIN ' . _DB_PREFIX_ . 'customer c
ON (IFNULL(ct.id_customer, ct.email) = IFNULL(c.id_customer, c.email))
WHERE ct.id_customer_thread = ' . (int) $id_customer_thread . '
ORDER BY cm.date_add ASC
');
}
public static function getNextThread($id_customer_thread)
{
$context = Context::getContext();
return Db::getInstance()->getValue('
SELECT id_customer_thread
FROM ' . _DB_PREFIX_ . 'customer_thread ct
WHERE ct.status = "open"
AND ct.date_upd > (
SELECT date_add FROM ' . _DB_PREFIX_ . 'customer_message
WHERE (id_employee IS NULL OR id_employee = 0)
AND id_customer_thread = ' . (int) $id_customer_thread . '
ORDER BY date_add DESC LIMIT 1
)
' . ($context->cookie->{'customer_threadFilter_cl!id_contact'} ?
'AND ct.id_contact = ' . (int) $context->cookie->{'customer_threadFilter_cl!id_contact'} : '') . '
' . ($context->cookie->{'customer_threadFilter_l!id_lang'} ?
'AND ct.id_lang = ' . (int) $context->cookie->{'customer_threadFilter_l!id_lang'} : '') .
' ORDER BY ct.date_upd ASC
');
}
public static function getCustomerMessagesOrder($id_customer, $id_order)
{
$sql = 'SELECT cm.*, c.`firstname` AS cfirstname, c.`lastname` AS clastname,
e.`firstname` AS efirstname, e.`lastname` AS elastname
FROM ' . _DB_PREFIX_ . 'customer_thread ct
LEFT JOIN ' . _DB_PREFIX_ . 'customer_message cm
ON ct.id_customer_thread = cm.id_customer_thread
LEFT JOIN `' . _DB_PREFIX_ . 'customer` c
ON ct.`id_customer` = c.`id_customer`
LEFT OUTER JOIN `' . _DB_PREFIX_ . 'employee` e
ON e.`id_employee` = cm.`id_employee`
WHERE ct.id_customer = ' . (int) $id_customer .
' AND ct.`id_order` = ' . (int) $id_order . '
GROUP BY cm.id_customer_message
ORDER BY cm.date_add DESC
LIMIT 2';
return Db::getInstance()->executeS($sql);
}
}

412
classes/Customization.php Normal file
View File

@@ -0,0 +1,412 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class CustomizationCore.
*/
class CustomizationCore extends ObjectModel
{
/** @var int */
public $id_product_attribute;
/** @var int */
public $id_address_delivery;
/** @var int */
public $id_cart;
/** @var int */
public $id_product;
/**
* @deprecated Since 9.0.0. Use the quantity from the table cart_product instead.
*
* @var int
*/
public $quantity;
/** @var int */
public $quantity_refunded;
/** @var int */
public $quantity_returned;
/** @var bool */
public $in_cart;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'customization',
'primary' => 'id_customization',
'fields' => [
/* Classic fields */
'id_product_attribute' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_address_delivery' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_cart' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_product' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'quantity' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'quantity_refunded' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'quantity_returned' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'in_cart' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'required' => true],
],
];
protected $webserviceParameters = [
'fields' => [
'id_address_delivery' => [
'xlink_resource' => [
'resourceName' => 'addresses',
],
],
'id_cart' => [
'xlink_resource' => [
'resourceName' => 'carts',
],
],
'id_product' => [
'xlink_resource' => [
'resourceName' => 'products',
],
],
],
'associations' => [
'customized_data_text_fields' => [
'resource' => 'customized_data_text_field',
'virtual_entity' => true,
'fields' => [
'id_customization_field' => ['required' => true, 'xlink_resource' => 'product_customization_fields'],
'value' => [],
],
],
'customized_data_images' => [
'resource' => 'customized_data_image',
'virtual_entity' => true,
'setter' => false,
'fields' => [
'id_customization_field' => ['xlink_resource' => 'product_customization_fields'],
'value' => [],
],
],
],
];
/**
* Get returned Customizations.
*
* @param int $idOrder Order ID
*
* @return array|bool
*/
public static function getReturnedCustomizations($idOrder)
{
if (($result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT ore.`id_order_return`, ord.`id_order_detail`, ord.`id_customization`, ord.`product_quantity`
FROM `' . _DB_PREFIX_ . 'order_return` ore
INNER JOIN `' . _DB_PREFIX_ . 'order_return_detail` ord ON (ord.`id_order_return` = ore.`id_order_return`)
WHERE ore.`id_order` = ' . (int) $idOrder . ' AND ord.`id_customization` != 0')) === false) {
return false;
}
$customizations = [];
foreach ($result as $row) {
$customizations[(int) $row['id_customization']] = $row;
}
return $customizations;
}
/**
* Get ordered Customizations.
*
* @param int $idCart Cart ID
*
* @return array|bool Ordered Customizations
* `false` if not found
*/
public static function getOrderedCustomizations($idCart)
{
if (!$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT `id_customization`, `quantity` FROM `' . _DB_PREFIX_ . 'customization` WHERE `id_cart` = ' . (int) $idCart)) {
return false;
}
$customizations = [];
foreach ($result as $row) {
$customizations[(int) $row['id_customization']] = $row;
}
return $customizations;
}
/**
* Get price of Customization.
*
* @param int $idCustomization Customization ID
*
* @return float|int Price of customization
*/
public static function getCustomizationPrice($idCustomization)
{
if (!(int) $idCustomization) {
return 0;
}
// For anyone migrating this - not sure why there is a SUM, when there can be only one line
return (float) Db::getInstance()->getValue(
'
SELECT SUM(`price`) FROM `' . _DB_PREFIX_ . 'customized_data`
WHERE `id_customization` = ' . (int) $idCustomization
);
}
/**
* Get weight of Customization.
*
* @param int $idCustomization Customization ID
*
* @return float|int Weight
*/
public static function getCustomizationWeight($idCustomization)
{
if (!(int) $idCustomization) {
return 0;
}
// For anyone migrating this - not sure why there is a SUM, when there can be only one line
return (float) Db::getInstance()->getValue(
'
SELECT SUM(`weight`) FROM `' . _DB_PREFIX_ . 'customized_data`
WHERE `id_customization` = ' . (int) $idCustomization
);
}
/**
* Count Customization quantity by Product.
*
* @param array $customizations Customizations
*
* @return array Customization quantities by Product
*/
public static function countCustomizationQuantityByProduct($customizations)
{
$total = [];
foreach ($customizations as $customization) {
$total[(int) $customization['id_order_detail']] = !isset($total[(int) $customization['id_order_detail']]) ? (int) $customization['quantity'] : $total[(int) $customization['id_order_detail']] + (int) $customization['quantity'];
}
return $total;
}
/**
* Get label.
*
* @param int $idCustomization Customization ID
* @param int $idLang Language IOD
* @param int|null $idShop Shop ID
*
* @return bool|false|string|null
*/
public static function getLabel($idCustomization, $idLang, $idShop = null)
{
if (!(int) $idCustomization || !(int) $idLang) {
return false;
}
if (Shop::isFeatureActive() && !(int) $idShop) {
$idShop = (int) Context::getContext()->shop->id;
}
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
'
SELECT `name`
FROM `' . _DB_PREFIX_ . 'customization_field_lang`
WHERE `id_customization_field` = ' . (int) $idCustomization . ((int) $idShop ? ' AND `id_shop` = ' . (int) $idShop : '') . '
AND `id_lang` = ' . (int) $idLang
);
return $result;
}
/**
* Retrieve quantities from IDs.
*
* @param array $idsCustomizations Customization IDs
*
* @return array Quantities
*/
public static function retrieveQuantitiesFromIds($idsCustomizations)
{
$quantities = [];
$inValues = '';
foreach ($idsCustomizations as $key => $idCustomization) {
if ($key > 0) {
$inValues .= ',';
}
$inValues .= (int) $idCustomization;
}
if (!empty($inValues)) {
$results = Db::getInstance()->executeS(
'SELECT `id_customization`, `id_product`, `quantity`, `quantity_refunded`, `quantity_returned`
FROM `' . _DB_PREFIX_ . 'customization`
WHERE `id_customization` IN (' . $inValues . ')'
);
foreach ($results as $row) {
$quantities[$row['id_customization']] = $row;
}
}
return $quantities;
}
/**
* Count quantity by Cart.
*
* @param int $idCart Cart ID
*
* @return array
*/
public static function countQuantityByCart($idCart)
{
$quantity = [];
$results = Db::getInstance()->executeS('
SELECT `id_product`, `id_product_attribute`, SUM(`quantity`) AS quantity
FROM `' . _DB_PREFIX_ . 'customization`
WHERE `id_cart` = ' . (int) $idCart . '
GROUP BY `id_cart`, `id_product`, `id_product_attribute`
');
foreach ($results as $row) {
$quantity[$row['id_product']][$row['id_product_attribute']] = $row['quantity'];
}
return $quantity;
}
/**
* This method is allow to know if a feature is used or active.
*
* @return bool
*/
public static function isFeatureActive()
{
return Configuration::get('PS_CUSTOMIZATION_FEATURE_ACTIVE');
}
/**
* This method is allow to know if a Customization entity is currently used.
*
* @param string|null $table Name of table linked to entity
* @param bool $hasActiveColumn True if the table has an active column
*
* @return bool
*/
public static function isCurrentlyUsed($table = null, $hasActiveColumn = false)
{
return (bool) Db::getInstance()->getValue('
SELECT `id_customization_field`
FROM `' . _DB_PREFIX_ . 'customization_field`
');
}
/**
* Get customized text fields
* (for webservice).
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public function getWsCustomizedDataTextFields()
{
if (!$results = Db::getInstance()->executeS('
SELECT id_customization_field, value
FROM `' . _DB_PREFIX_ . 'customization_field` cf
LEFT JOIN `' . _DB_PREFIX_ . 'customized_data` cd ON (cf.id_customization_field = cd.index)
WHERE `id_product` = ' . (int) $this->id_product . '
AND id_customization = ' . (int) $this->id . '
AND cf.type = ' . (int) Product::CUSTOMIZE_TEXTFIELD)) {
return [];
}
return $results;
}
/**
* Get customized images data
* (for webservice).
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public function getWsCustomizedDataImages()
{
if (!$results = Db::getInstance()->executeS('
SELECT id_customization_field, value
FROM `' . _DB_PREFIX_ . 'customization_field` cf
LEFT JOIN `' . _DB_PREFIX_ . 'customized_data` cd ON (cf.id_customization_field = cd.index)
WHERE `id_product` = ' . (int) $this->id_product . '
AND id_customization = ' . (int) $this->id . '
AND cf.type = ' . (int) Product::CUSTOMIZE_FILE)) {
return [];
}
return $results;
}
/**
* Set customized text fields
* (for webservice).
*
* @param array $values
*
* @return bool
*/
public function setWsCustomizedDataTextFields($values)
{
$cart = new Cart($this->id_cart);
if (!Validate::isLoadedObject($cart)) {
WebserviceRequest::getInstance()->setError(500, $this->trans('Could not load cart id=%s', [$this->id_cart], 'Admin.Notifications.Error'), 137);
return false;
}
Db::getInstance()->execute('
DELETE FROM `' . _DB_PREFIX_ . 'customized_data`
WHERE id_customization = ' . (int) $this->id . '
AND type = ' . (int) Product::CUSTOMIZE_TEXTFIELD);
foreach ($values as $value) {
$query = 'INSERT INTO `' . _DB_PREFIX_ . 'customized_data` (`id_customization`, `type`, `index`, `value`)
VALUES (' . (int) $this->id . ', ' . (int) Product::CUSTOMIZE_TEXTFIELD . ', ' . (int) $value['id_customization_field'] . ', \'' . pSQL($value['value']) . '\')';
if (!Db::getInstance()->execute($query)) {
return false;
}
}
return true;
}
/**
* Delete the current context shops langs.
*
* @param int $idCustomizationField
* @param int[] $shopList
*
* @return bool
*
* @throws PrestaShopDatabaseException
*/
public static function deleteCustomizationFieldLangByShop($idCustomizationField, $shopList)
{
$return = Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customization_field_lang`
WHERE `id_customization_field` = ' . (int) $idCustomizationField . '
AND `id_shop` IN (' . implode(',', $shopList) . ')');
if (!$return) {
throw new PrestaShopDatabaseException('An error occurred while deletion the customization fields lang');
}
return $return;
}
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Core\Domain\Product\Customization\CustomizationFieldSettings;
/**
* Class CustomizationFieldCore.
*/
class CustomizationFieldCore extends ObjectModel
{
/** @var int */
public $id_product;
/** @var int Customization type (0 File, 1 Textfield) (See Product class) */
public $type;
/** @var bool Field is required */
public $required;
/** @var bool Field was added by a module */
public $is_module;
/** @var string[] Label for customized field */
public $name;
/** @var bool Soft delete */
public $is_deleted;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'customization_field',
'primary' => 'id_customization_field',
'multilang' => true,
'multilang_shop' => true,
'fields' => [
/* Classic fields */
'id_product' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'type' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'required' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'required' => true],
'is_module' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'required' => false],
'is_deleted' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'required' => false],
/* Lang fields */
'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'required' => true, 'size' => CustomizationFieldSettings::MAX_NAME_LENGTH],
],
];
/** @var array */
protected $webserviceParameters = [
'fields' => [
'id_product' => [
'xlink_resource' => [
'resourceName' => 'products',
],
],
],
];
}

54
classes/DateRange.php Normal file
View File

@@ -0,0 +1,54 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class DateRangeCore.
*/
class DateRangeCore extends ObjectModel
{
/** @var string */
public $time_start;
/** @var string */
public $time_end;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'date_range',
'primary' => 'id_date_range',
'fields' => [
'time_start' => ['type' => self::TYPE_DATE, 'validate' => 'isDate', 'required' => true],
'time_end' => ['type' => self::TYPE_DATE, 'validate' => 'isDate', 'required' => true],
],
];
/**
* Get current range.
*
* @return mixed
*/
public static function getCurrentRange()
{
$result = Db::getInstance()->getRow('
SELECT `id_date_range`, `time_end`
FROM `' . _DB_PREFIX_ . 'date_range`
WHERE `time_end` = (SELECT MAX(`time_end`) FROM `' . _DB_PREFIX_ . 'date_range`)');
if (!isset($result['id_date_range']) || strtotime($result['time_end']) < strtotime(date('Y-m-d H:i:s'))) {
// The default range is set to 1 day less 1 second (in seconds)
$rangeSize = 86399;
$dateRange = new DateRange();
$dateRange->time_start = date('Y-m-d');
$dateRange->time_end = strftime('%Y-%m-%d %H:%M:%S', strtotime($dateRange->time_start) + $rangeSize);
$dateRange->add();
return $dateRange->id;
}
return $result['id_date_range'];
}
}

87
classes/Delivery.php Normal file
View File

@@ -0,0 +1,87 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class DeliveryCore.
*/
class DeliveryCore extends ObjectModel
{
/** @var int */
public $id_delivery;
/** @var int * */
public $id_shop;
/** @var int * */
public $id_shop_group;
/** @var int */
public $id_carrier;
/** @var int */
public $id_range_price;
/** @var int */
public $id_range_weight;
/** @var int */
public $id_zone;
/** @var float */
public $price;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'delivery',
'primary' => 'id_delivery',
'fields' => [
'id_carrier' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_range_price' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_range_weight' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_zone' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_shop' => ['type' => self::TYPE_INT],
'id_shop_group' => ['type' => self::TYPE_INT],
'price' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true],
],
];
protected $webserviceParameters = [
'objectsNodeName' => 'deliveries',
'fields' => [
'id_carrier' => ['xlink_resource' => 'carriers'],
'id_range_price' => ['xlink_resource' => 'price_ranges'],
'id_range_weight' => ['xlink_resource' => 'weight_ranges'],
'id_zone' => ['xlink_resource' => 'zones'],
],
];
/**
* Get Object fields and values in array.
*
* @return array
*/
public function getFields()
{
$fields = parent::getFields();
// @todo add null management in definitions
if ($this->id_shop) {
$fields['id_shop'] = (int) $this->id_shop;
} else {
$fields['id_shop'] = null;
}
if ($this->id_shop_group) {
$fields['id_shop_group'] = (int) $this->id_shop_group;
} else {
$fields['id_shop_group'] = null;
}
return $fields;
}
}

1340
classes/Dispatcher.php Normal file

File diff suppressed because it is too large Load Diff

748
classes/Employee.php Normal file
View File

@@ -0,0 +1,748 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Adapter\CoreException;
use PrestaShop\PrestaShop\Adapter\ServiceLocator;
use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
use PrestaShop\PrestaShop\Core\Crypto\Hashing;
/**
* Class EmployeeCore.
*/
class EmployeeCore extends ObjectModel
{
/** @var int|null Employee ID */
public $id;
/** @var int Employee profile */
public $id_profile;
/** @var int Employee language */
public $id_lang;
/** @var string Lastname */
public $lastname;
/** @var string Firstname */
public $firstname;
/** @var string e-mail */
public $email;
/** @var string Password */
public $passwd;
/** @var string Password */
public $last_passwd_gen;
public $stats_date_from;
public $stats_date_to;
public $stats_compare_from;
public $stats_compare_to;
public $stats_compare_option = 1;
public $preselect_date_range;
/** @var string Display back office background in the specified color */
public $bo_color;
public $default_tab;
/** @var string employee's chosen theme */
public $bo_theme;
/** @var string employee's chosen css file */
public $bo_css = 'theme.css';
/** @var int employee desired screen width */
public $bo_width;
/** @var bool */
public $bo_menu = true;
/** @var bool Status */
public $active = true;
public $remote_addr;
/* employee notifications */
public $id_last_order;
public $id_last_customer_message;
public $id_last_customer;
/** @var string|null Unique token for forgot password feature */
public $reset_password_token;
/** @var string|null token validity date for forgot password feature */
public $reset_password_validity;
/**
* @var bool
*/
public $has_enabled_gravatar = false;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'employee',
'primary' => 'id_employee',
'fields' => [
'lastname' => ['type' => self::TYPE_STRING, 'validate' => 'isName', 'required' => true, 'size' => 255],
'firstname' => ['type' => self::TYPE_STRING, 'validate' => 'isName', 'required' => true, 'size' => 255],
'email' => ['type' => self::TYPE_STRING, 'validate' => 'isEmail', 'required' => true, 'size' => 255],
'id_lang' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'required' => true],
'passwd' => ['type' => self::TYPE_STRING, 'validate' => 'isHashedPassword', 'required' => true, 'size' => 255],
'last_passwd_gen' => ['type' => self::TYPE_STRING],
'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'id_profile' => ['type' => self::TYPE_INT, 'validate' => 'isInt', 'required' => true],
'bo_color' => ['type' => self::TYPE_STRING, 'validate' => 'isColor', 'size' => 32],
'default_tab' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
'bo_theme' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 32],
'bo_css' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 64],
'bo_width' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
'bo_menu' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'stats_date_from' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'stats_date_to' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'stats_compare_from' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'stats_compare_to' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'stats_compare_option' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
'preselect_date_range' => ['type' => self::TYPE_STRING, 'size' => 32],
'id_last_order' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
'id_last_customer_message' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
'id_last_customer' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
'reset_password_token' => ['type' => self::TYPE_STRING, 'validate' => 'isSha1', 'size' => 40, 'copy_post' => false],
'reset_password_validity' => ['type' => self::TYPE_DATE, 'validate' => 'isDateOrNull', 'copy_post' => false],
'has_enabled_gravatar' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
],
];
protected $webserviceParameters = [
'fields' => [
'id_lang' => ['xlink_resource' => 'languages'],
'last_passwd_gen' => ['setter' => false],
'stats_date_from' => ['setter' => false],
'stats_date_to' => ['setter' => false],
'stats_compare_from' => ['setter' => false],
'stats_compare_to' => ['setter' => false],
'passwd' => ['setter' => 'setWsPasswd'],
],
];
protected $associated_shops = [];
/**
* EmployeeCore constructor.
*
* @param int|null $id Employee ID
* @param int|null $idLang Language ID
* @param int|null $idShop Shop ID
*/
public function __construct($id = null, $idLang = null, $idShop = null)
{
parent::__construct($id, null, $idShop);
if (null !== $idLang) {
$this->id_lang = (int) (Language::getLanguage($idLang) !== false) ? $idLang : Configuration::get('PS_LANG_DEFAULT');
}
if ($this->id) {
$this->associated_shops = $this->getAssociatedShops();
}
$this->image_dir = _PS_EMPLOYEE_IMG_DIR_;
}
/**
* @see ObjectModel::getFields()
*
* @return array
*/
public function getFields()
{
if (empty($this->stats_date_from) || $this->stats_date_from == '0000-00-00') {
$this->stats_date_from = date('Y-m-d', strtotime('-1 month'));
}
if (empty($this->stats_compare_from) || $this->stats_compare_from == '0000-00-00') {
$this->stats_compare_from = null;
}
if (empty($this->stats_date_to) || $this->stats_date_to == '0000-00-00') {
$this->stats_date_to = date('Y-m-d');
}
if (empty($this->stats_compare_to) || $this->stats_compare_to == '0000-00-00') {
$this->stats_compare_to = null;
}
return parent::getFields();
}
/**
* Adds current Employee as a new Object to the database.
*
* @param bool $autoDate Automatically set `date_upd` and `date_add` columns
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Indicates whether the Employee has been successfully added
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function add($autoDate = true, $nullValues = true)
{
$this->last_passwd_gen = date('Y-m-d H:i:s', strtotime('-' . Configuration::get('PS_PASSWD_TIME_BACK') . 'minutes'));
$this->updateTextDirection();
return parent::add($autoDate, $nullValues);
}
/**
* Updates the current object in the database.
*
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Indicates whether the Employee has been successfully updated
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function update($nullValues = false)
{
if (empty($this->stats_date_from) || $this->stats_date_from == '0000-00-00') {
$this->stats_date_from = date('Y-m-d');
}
if (empty($this->stats_date_to) || $this->stats_date_to == '0000-00-00') {
$this->stats_date_to = date('Y-m-d');
}
$currentEmployee = new Employee((int) $this->id);
$this->updateTextDirection();
return parent::update($nullValues);
}
/**
* Update Employee text direction.
*/
protected function updateTextDirection()
{
if (!defined('_PS_ADMIN_DIR_')) {
return;
}
$path = _PS_ADMIN_DIR_ . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $this->bo_theme . DIRECTORY_SEPARATOR . 'css' . DIRECTORY_SEPARATOR;
$language = new Language($this->id_lang);
if ($language->is_rtl && !strpos($this->bo_css, '_rtl')) {
$boCss = preg_replace('/^(.*)\.css$/', '$1_rtl.css', $this->bo_css);
if (file_exists($path . $boCss)) {
$this->bo_css = $boCss;
}
} elseif (!$language->is_rtl && strpos($this->bo_css, '_rtl')) {
$boCss = preg_replace('/^(.*)_rtl\.css$/', '$1.css', $this->bo_css);
if (file_exists($path . $boCss)) {
$this->bo_css = $boCss;
}
}
}
/**
* Return list of employees.
*
* @param bool $activeOnly Filter employee by active status
*
* @return array|false Employees or false
*/
public static function getEmployees($activeOnly = true)
{
return Db::getInstance()->executeS('
SELECT `id_employee`, `firstname`, `lastname`
FROM `' . _DB_PREFIX_ . 'employee`
' . ($activeOnly ? ' WHERE `active` = 1' : '') . '
ORDER BY `lastname` ASC
');
}
/**
* Return employee instance from its e-mail (optionally check password).
*
* @param string $email e-mail
* @param string $plaintextPassword Password is also checked if specified
* @param bool $activeOnly Filter employee by active status
*
* @return bool|Employee|EmployeeCore Employee instance
* `false` if not found
*/
public function getByEmail($email, $plaintextPassword = null, $activeOnly = true)
{
if (!Validate::isEmail($email)) {
throw new PrestaShopException('Email address is invalid.');
}
$sql = new DbQuery();
$sql->select('e.*');
$sql->from('employee', 'e');
$sql->where('e.`email` = \'' . pSQL($email) . '\'');
if ($activeOnly) {
$sql->where('e.`active` = 1');
}
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql);
if (!$result) {
// Create fake result to make sure computing time does not allow password enumeration
$result = ['passwd' => '123456'];
}
/** @var Hashing $crypto */
$crypto = ServiceLocator::get(Hashing::class);
$passwordHash = $result['passwd'];
$shouldCheckPassword = null !== $plaintextPassword;
if ($shouldCheckPassword && !$crypto->checkHash($plaintextPassword, $passwordHash)) {
return false;
}
$this->id = (int) $result['id_employee'];
$this->id_profile = (int) $result['id_profile'];
foreach ($result as $key => $value) {
if (property_exists($this, $key)) {
$this->{$key} = $value;
}
}
if ($shouldCheckPassword && !$crypto->isFirstHash($plaintextPassword, $passwordHash)) {
$this->passwd = $crypto->hash($plaintextPassword);
$this->update();
}
return $this;
}
/**
* Check if Employee exists.
*
* @param string $email Employee email
*
* @return bool Indicates whether the Employee exists
*/
public static function employeeExists($email)
{
if (!Validate::isEmail($email)) {
throw new PrestaShopException('Email address is invalid.');
}
return (bool) Db::getInstance()->getValue('
SELECT `id_employee`
FROM `' . _DB_PREFIX_ . 'employee`
WHERE `email` = \'' . pSQL($email) . '\'
', false);
}
/**
* Check if employee password is the right one.
*
* @param string $passwordHash Password
*
* @return bool result
*/
public static function checkPassword($idEmployee, $passwordHash)
{
if (!Validate::isUnsignedId($idEmployee)) {
throw new PrestaShopException('Employee ID is invalid.');
}
$sql = new DbQuery();
$sql->select('e.`id_employee`');
$sql->from('employee', 'e');
$sql->where('e.`id_employee` = ' . (int) $idEmployee);
$sql->where('e.`passwd` = \'' . pSQL($passwordHash) . '\'');
$sql->where('e.`active` = 1');
// Get result from DB
return Db::getInstance()->getValue($sql);
}
/**
* Count amount of Employees with the given Profile ID.
*
* @param int $idProfile Profile ID
* @param bool $activeOnly Only active Employees
*
* @return false|string|null
*/
public static function countProfile($idProfile, $activeOnly = false)
{
return Db::getInstance()->getValue(
'
SELECT COUNT(*)
FROM `' . _DB_PREFIX_ . 'employee`
WHERE `id_profile` = ' . (int) $idProfile . '
' . ($activeOnly ? ' AND `active` = 1' : '')
);
}
/**
* Check if this Employee is the only SuperAdmin left.
*
* @return bool Indicates whether this Employee is the last one
*/
public function isLastAdmin()
{
return $this->isSuperAdmin()
&& Employee::countProfile($this->id_profile, true) == 1
&& $this->active;
}
/**
* Set password
* (for webservice).
*
* @param string $passwd Password
*
* @return bool Indicates whether the password was succesfully set
*/
public function setWsPasswd($passwd)
{
try {
/** @var Hashing $crypto */
$crypto = ServiceLocator::get('\\PrestaShop\\PrestaShop\\Core\\Crypto\\Hashing');
} catch (CoreException $e) {
return false;
}
if ($this->id != 0) {
if ($this->passwd != $passwd) {
$this->passwd = $crypto->hash($passwd);
}
} else {
$this->passwd = $crypto->hash($passwd);
}
return true;
}
/**
* Check employee informations saved into cookie and return employee validity.
*
* @return bool employee validity
*/
public function isLoggedBack()
{
$container = SymfonyContainer::getInstance();
if (!$container) {
return false;
}
$userProvider = $container->get('prestashop.user_provider');
return $userProvider->getUser() !== null;
}
/**
* Logout.
*/
public function logout()
{
if (isset(Context::getContext()->cookie)) {
Context::getContext()->cookie->logout();
Context::getContext()->cookie->write();
}
$sfContainer = SymfonyContainer::getInstance();
if ($sfContainer !== null) {
$sfContainer->get('prestashop.user_provider')->logout();
}
$this->id = null;
}
/**
* Get favorite Module list.
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public function favoriteModulesList()
{
return Db::getInstance()->executeS(
'
SELECT `module`
FROM `' . _DB_PREFIX_ . 'module_preference`
WHERE `id_employee` = ' . (int) $this->id . ' AND `favorite` = 1 AND (`interest` = 1 OR `interest` IS NULL)'
);
}
/**
* Check if the employee is associated to a specific shop.
*
* @param int $idShop
*
* @return bool
*/
public function hasAuthOnShop($idShop)
{
return $this->isSuperAdmin() || in_array($idShop, $this->associated_shops);
}
/**
* Check if the employee is associated to a specific shop group.
*
* @param int $idShopGroup ShopGroup ID
*
* @return bool
*/
public function hasAuthOnShopGroup($idShopGroup)
{
if ($this->isSuperAdmin()) {
return true;
}
foreach ($this->associated_shops as $idShop) {
/** @var int $groupFromShop */
$groupFromShop = Shop::getGroupFromShop($idShop, true);
if ($idShopGroup == $groupFromShop) {
return true;
}
}
return false;
}
/**
* Get default id_shop with auth for current employee.
*
* @return int
*/
public function getDefaultShopID()
{
if ($this->isSuperAdmin() || in_array(Configuration::get('PS_SHOP_DEFAULT'), $this->associated_shops)) {
return (int) Configuration::get('PS_SHOP_DEFAULT');
}
return $this->associated_shops[0];
}
/**
* Get Employees by Profile.
*
* @param int $idProfile Profile ID
* @param bool $activeOnly Only active Employees
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public static function getEmployeesByProfile($idProfile, $activeOnly = false)
{
return Db::getInstance()->executeS(
'
SELECT *
FROM `' . _DB_PREFIX_ . 'employee`
WHERE `id_profile` = ' . (int) $idProfile . '
' . ($activeOnly ? ' AND `active` = 1' : '')
);
}
/**
* Check if current employee is super administrator.
*
* @return bool
*/
public function isSuperAdmin()
{
return $this->id_profile == _PS_ADMIN_PROFILE_;
}
/**
* Get Employee image.
*
* @return string Image URL
*/
public function getImage()
{
$defaultSystem = Tools::getAdminImageUrl('pr/default.jpg');
// Default from Profile
$profile = new Profile($this->id_profile);
$imageUrl = (int) $profile->id === (int) $this->id_profile ? $profile->getProfileImage() : null;
// Gravatar
if ($this->has_enabled_gravatar) {
$imageUrl = $imageUrl ?? 'https://www.gravatar.com/avatar/' . md5(strtolower(trim($this->email))) . '?d=' . urlencode($defaultSystem);
}
// Local Image
$imagePath = $this->image_dir . $this->id . '.jpg';
if (file_exists($imagePath)) {
$imageUrl = $imageUrl ?? Context::getContext()->link->getMediaLink(
str_replace($this->image_dir, _THEME_EMPLOYEE_DIR_, $imagePath)
);
}
// Default from System
$imageUrl = $imageUrl ?? $defaultSystem;
// Hooks
Hook::exec(
'actionOverrideEmployeeImage',
[
'employee' => $this,
'imageUrl' => &$imageUrl,
]
);
return $imageUrl;
}
/**
* Get last elements for notify.
*
* @param string $element
*
* @return int
*/
public function getLastElementsForNotify($element)
{
$element = bqSQL($element);
$max = Db::getInstance()->getValue('
SELECT MAX(`id_' . $element . '`) as `id_' . $element . '`
FROM `' . _DB_PREFIX_ . $element . ($element == 'order' ? 's' : '') . '`');
// if no rows in table, set max to 0
if ((int) $max < 1) {
$max = 0;
}
return (int) $max;
}
/**
* Set last connection date.
*
* @param int $idEmployee Employee ID
*
* @return bool
*/
public static function setLastConnectionDate($idEmployee)
{
return Db::getInstance()->execute('
UPDATE `' . _DB_PREFIX_ . 'employee`
SET `last_connection_date` = CURRENT_DATE()
WHERE `id_employee` = ' . (int) $idEmployee . '
AND (`last_connection_date` < CURRENT_DATE()
OR `last_connection_date` IS NULL)
');
}
/**
* Fill Reset password unique token with random sha1 and its validity date. For forgot password feature.
*/
public function stampResetPasswordToken()
{
$salt = $this->id . '+' . uniqid((string) mt_rand(0, mt_getrandmax()), true);
$this->reset_password_token = sha1(time() . $salt);
$validity = (int) Configuration::get('PS_PASSWD_RESET_VALIDITY') ?: 1440;
$this->reset_password_validity = date('Y-m-d H:i:s', strtotime('+' . $validity . ' minutes'));
}
/**
* Test if a reset password token is present and is recent enough to avoid creating a new one (in case of employee triggering the forgot password link too often).
*/
public function hasRecentResetPasswordToken()
{
if (!$this->reset_password_token) {
return false;
}
// TODO maybe use another 'recent' value for this test. For instance, equals password validity value.
if (!$this->reset_password_validity || strtotime($this->reset_password_validity) < time()) {
return false;
}
return true;
}
/**
* Returns the valid reset password token if it validity date is > now().
*/
public function getValidResetPasswordToken()
{
if (!$this->reset_password_token) {
return false;
}
if (!$this->reset_password_validity || strtotime($this->reset_password_validity) < time()) {
return false;
}
return $this->reset_password_token;
}
/**
* Delete reset password token data.
*/
public function removeResetPasswordToken()
{
$this->reset_password_token = null;
$this->reset_password_validity = null;
}
/**
* Is the Employee allowed to do the given action.
*
* @param string $action
* @param string $tab
*
* @return bool
*/
public function can($action, $tab)
{
$access = Profile::getProfileAccess($this->id_profile, Tab::getIdFromClassName($tab));
return is_array($access) && $access[$action] == '1';
}
/**
* Returns the default tab class name.
*
* @return string|null
*/
public function getDefaultTabClassName()
{
if ($tabId = (int) $this->default_tab) {
return Tab::getClassNameById($tabId) ?: null;
}
return null;
}
public function getAssociatedShopIds(): array
{
return $this->associated_shops;
}
public function getAssociatedShopGroupIds(): array
{
$associatedShopGroupIds = [];
foreach ($this->associated_shops as $shopId) {
/** @var int $groupFromShop */
$groupFromShop = Shop::getGroupFromShop($shopId, true);
if (!empty($groupFromShop) && !in_array($groupFromShop, $associatedShopGroupIds)) {
$associatedShopGroupIds[] = (int) $groupFromShop;
}
}
return $associatedShopGroupIds;
}
public function getImageUrl(): string
{
return $this->getImage();
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Core\Session\SessionInterface;
class EmployeeSessionCore extends ObjectModel implements SessionInterface
{
public $id;
/** @var int Id Employee */
public $id_employee;
/** @var string Token */
public $token;
/** @var string Object last modification date */
public $date_upd;
/** @var string Object creation date */
public $date_add;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'employee_session',
'primary' => 'id_employee_session',
'fields' => [
'id_employee' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'token' => ['type' => self::TYPE_STRING, 'validate' => 'isSha1', 'size' => 40, 'copy_post' => false],
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate', 'copy_post' => false],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate', 'copy_post' => false],
],
];
/**
* {@inheritdoc}
*/
public function getId()
{
return $this->id;
}
/**
* {@inheritdoc}
*/
public function setUserId($idEmployee)
{
$this->id_employee = (int) $idEmployee;
}
/**
* {@inheritdoc}
*/
public function getUserId()
{
return (int) $this->id_employee;
}
/**
* {@inheritdoc}
*/
public function setToken($token)
{
$this->token = (string) $token;
}
/**
* {@inheritdoc}
*/
public function getToken()
{
return $this->token;
}
}

354
classes/Feature.php Normal file
View File

@@ -0,0 +1,354 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class FeatureCore.
*/
class FeatureCore extends ObjectModel
{
/** @var string|array<int, string> Name */
public $name;
/** @var int */
public $position;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'feature',
'primary' => 'id_feature',
'multilang' => true,
'fields' => [
'position' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
/* Lang fields */
'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 128],
],
];
protected $webserviceParameters = [
'objectsNodeName' => 'product_features',
'objectNodeName' => 'product_feature',
'fields' => [],
];
/**
* Get a feature data for a given id_feature and id_lang.
*
* @param int $idLang Language ID
* @param int $idFeature Feature ID
*
* @return array Array with feature's data
*/
public static function getFeature($idLang, $idFeature)
{
return Db::getInstance()->getRow(
'
SELECT *
FROM `' . _DB_PREFIX_ . 'feature` f
LEFT JOIN `' . _DB_PREFIX_ . 'feature_lang` fl
ON ( f.`id_feature` = fl.`id_feature` AND fl.`id_lang` = ' . (int) $idLang . ')
WHERE f.`id_feature` = ' . (int) $idFeature
);
}
/**
* Get all features for a given language.
*
* @param int $idLang Language id
*
* @return array Multiple arrays with feature's data
*/
public static function getFeatures($idLang, $withShop = true)
{
return Db::getInstance()->executeS('
SELECT DISTINCT f.id_feature, f.*, fl.*
FROM `' . _DB_PREFIX_ . 'feature` f
' . ($withShop ? Shop::addSqlAssociation('feature', 'f') : '') . '
LEFT JOIN `' . _DB_PREFIX_ . 'feature_lang` fl ON (f.`id_feature` = fl.`id_feature` AND fl.`id_lang` = ' . (int) $idLang . ')
ORDER BY f.`position` ASC');
}
/**
* Delete several objects from database.
*
* @param array $selection Array with items to delete
*
* @return bool Deletion result
*/
public function deleteSelection(array $selection)
{
/* Also delete Attributes */
foreach ($selection as $value) {
$obj = new Feature($value);
if (!$obj->delete()) {
return false;
}
}
return true;
}
/**
* Adds current Feature as a new Object to the database.
*
* @param bool $autoDate Automatically set `date_upd` and `date_add` columns
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Indicates whether the Feature has been successfully added
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function add($autoDate = true, $nullValues = false)
{
if ($this->position <= 0) {
$this->position = Feature::getHigherPosition() + 1;
}
$return = parent::add($autoDate, true);
Hook::exec('actionFeatureSave', ['id_feature' => $this->id]);
return $return;
}
/**
* Updates the current Feature in the database.
*
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Indicates whether the Feature has been successfully updated
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function update($nullValues = false)
{
$this->clearCache();
$result = true;
$fields = $this->getFieldsLang();
foreach ($fields as $field) {
foreach (array_keys($field) as $key) {
if (!Validate::isTableOrIdentifier($key)) {
throw new PrestaShopException('Invalid column name in feature_lang table.');
}
}
$sql = 'SELECT `id_lang` FROM `' . pSQL(_DB_PREFIX_ . $this->def['table']) . '_lang`
WHERE `' . $this->def['primary'] . '` = ' . (int) $this->id . '
AND `id_lang` = ' . (int) $field['id_lang'];
$mode = Db::getInstance()->getRow($sql);
$result = $result
&& (!$mode
? Db::getInstance()->insert($this->def['table'] . '_lang', $field)
: Db::getInstance()->update(
$this->def['table'] . '_lang',
$field,
'`' . $this->def['primary'] . '` = ' . (int) $this->id . ' AND `id_lang` = ' . (int) $field['id_lang']
)
);
}
if ($result) {
$result = parent::update($nullValues);
if ($result) {
Hook::exec('actionFeatureSave', ['id_feature' => $this->id]);
}
}
return $result;
}
/**
* Deletes current Feature from the database.
*
* @return bool `true` if delete was successful
*
* @throws PrestaShopException
*/
public function delete()
{
/* Also delete related attributes */
Db::getInstance()->execute('
DELETE
`' . _DB_PREFIX_ . 'feature_value_lang`
FROM
`' . _DB_PREFIX_ . 'feature_value_lang`
JOIN `' . _DB_PREFIX_ . 'feature_value`
ON (`' . _DB_PREFIX_ . 'feature_value_lang`.id_feature_value = `' . _DB_PREFIX_ . 'feature_value`.id_feature_value)
WHERE
`' . _DB_PREFIX_ . 'feature_value`.`id_feature` = ' . (int) $this->id . '
');
Db::getInstance()->execute(
'
DELETE FROM `' . _DB_PREFIX_ . 'feature_value`
WHERE `id_feature` = ' . (int) $this->id
);
// Also delete related products
Db::getInstance()->execute(
'
DELETE FROM `' . _DB_PREFIX_ . 'feature_product`
WHERE `id_feature` = ' . (int) $this->id
);
$return = parent::delete();
if ($return) {
Hook::exec('actionFeatureDelete', ['id_feature' => $this->id]);
}
/* Reinitializing position */
$this->cleanPositions();
return $return;
}
/**
* Count number of features for a given language.
*
* @param int $idLang Language id
*
* @return int Number of feature
*/
public static function nbFeatures($idLang)
{
return (int) Db::getInstance()->getValue('
SELECT COUNT(*) as nb
FROM `' . _DB_PREFIX_ . 'feature` ag
LEFT JOIN `' . _DB_PREFIX_ . 'feature_lang` agl
ON (ag.`id_feature` = agl.`id_feature` AND `id_lang` = ' . (int) $idLang . ')
');
}
/**
* Create a feature from import.
*
* @param string $name Feature name
* @param bool|int $position Feature position
*
* @return int Feature ID
*/
public static function addFeatureImport($name, $position = false)
{
$rq = Db::getInstance()->getRow('
SELECT `id_feature`
FROM ' . _DB_PREFIX_ . 'feature_lang
WHERE `name` = \'' . pSQL($name) . '\'
GROUP BY `id_feature`
');
if (empty($rq)) {
// Feature doesn't exist, create it
$feature = new Feature();
$feature->name = array_fill_keys(Language::getIDs(), (string) $name);
if ($position) {
$feature->position = (int) $position;
} else {
$feature->position = Feature::getHigherPosition() + 1;
}
$feature->add();
return $feature->id;
} elseif (isset($rq['id_feature']) && $rq['id_feature']) {
if (is_numeric($position)) {
$feature = new Feature((int) $rq['id_feature']);
$feature->position = (int) $position;
if (Validate::isLoadedObject($feature)) {
$feature->update();
}
}
return (int) $rq['id_feature'];
}
return 0;
}
/**
* This metohd is allow to know if a feature is used or active.
*
* @return bool
*/
public static function isFeatureActive()
{
return (bool) Configuration::get('PS_FEATURE_FEATURE_ACTIVE');
}
/**
* Move a feature.
*
* @param bool $way Up (1) or Down (0)
* @param int|null $position
* @param int|null $idFeature
*
* @return bool Update result
*/
public function updatePosition($way, $position, $idFeature = null)
{
if (!$res = Db::getInstance()->executeS(
'
SELECT `position`, `id_feature`
FROM `' . _DB_PREFIX_ . 'feature`
WHERE `id_feature` = ' . (int) ($idFeature ? $idFeature : $this->id) . '
ORDER BY `position` ASC'
)) {
return false;
}
foreach ($res as $feature) {
if ((int) $feature['id_feature'] == (int) $this->id) {
$moved_feature = $feature;
}
}
if (!isset($moved_feature) || !isset($position)) {
return false;
}
// < and > statements rather than BETWEEN operator
// since BETWEEN is treated differently according to databases
return Db::getInstance()->execute('
UPDATE `' . _DB_PREFIX_ . 'feature`
SET `position`= `position` ' . ($way ? '- 1' : '+ 1') . '
WHERE `position`
' . ($way
? '> ' . (int) $moved_feature['position'] . ' AND `position` <= ' . (int) $position
: '< ' . (int) $moved_feature['position'] . ' AND `position` >= ' . (int) $position))
&& Db::getInstance()->execute('
UPDATE `' . _DB_PREFIX_ . 'feature`
SET `position` = ' . (int) $position . '
WHERE `id_feature`=' . (int) $moved_feature['id_feature']);
}
/**
* Reorder feature position
* Call it after deleting a feature.
*
* @return bool $return
*/
public static function cleanPositions()
{
Db::getInstance()->execute('SET @i = -1', false);
$sql = 'UPDATE `' . _DB_PREFIX_ . 'feature` SET `position` = @i:=@i+1 ORDER BY `position` ASC';
return (bool) Db::getInstance()->execute($sql);
}
/**
* getHigherPosition.
*
* Get the higher feature position
*
* @return int $position
*/
public static function getHigherPosition()
{
$sql = 'SELECT MAX(`position`)
FROM `' . _DB_PREFIX_ . 'feature`';
$position = Db::getInstance()->getValue($sql);
return (is_numeric($position)) ? $position : -1;
}
}

98
classes/FeatureFlag.php Normal file
View File

@@ -0,0 +1,98 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
declare(strict_types=1);
use PrestaShop\PrestaShop\Core\FeatureFlag\FeatureFlagSettings;
/**
* Class FeatureFlagCore even if the feature flag is mostly handled via its Doctrine entity, we need this legacy class
* to handle the data installation for this entity.
*/
class FeatureFlagCore extends ObjectModel
{
/**
* @var string
*/
public $name;
/**
* @var string
*/
public $type;
/**
* @var string
*/
public $label_wording;
/**
* @var string
*/
public $label_domain;
/**
* @var string
*/
public $description_wording;
/**
* @var string
*/
public $description_domain;
/**
* @var bool
*/
public $state = false;
/**
* @var string
*/
public $stability;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'feature_flag',
'primary' => 'id_feature_flag',
'fields' => [
'name' => ['type' => self::TYPE_STRING, 'required' => true, 'size' => 191],
'type' => [
'type' => self::TYPE_STRING,
'required' => true,
'size' => 64,
'default' => FeatureFlagSettings::TYPE_DEFAULT,
],
'label_wording' => ['type' => self::TYPE_STRING, 'required' => true, 'size' => 191],
'label_domain' => ['type' => self::TYPE_STRING, 'required' => true, 'size' => 255],
'description_wording' => ['type' => self::TYPE_STRING, 'required' => true, 'size' => 191],
'description_domain' => ['type' => self::TYPE_STRING, 'required' => true, 'size' => 255],
'state' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'stability' => [
'type' => self::TYPE_STRING,
'size' => 64,
'values' => [
FeatureFlagSettings::STABILITY_STABLE,
FeatureFlagSettings::STABILITY_BETA,
],
'default' => FeatureFlagSettings::STABILITY_BETA,
],
],
];
public static function isEnabled(string $name): bool
{
$query = sprintf(
"SELECT state FROM %sfeature_flag WHERE name = '%s'",
_DB_PREFIX_,
pSQL($name)
);
return (bool) Db::getInstance()->getValue($query);
}
}

296
classes/FeatureValue.php Normal file
View File

@@ -0,0 +1,296 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Core\Domain\Feature\FeatureValueSettings;
/**
* Class FeatureValueCore.
*/
class FeatureValueCore extends ObjectModel
{
/** @var int Group id which attribute belongs */
public $id_feature;
/** @var string|array Name */
public $value;
/** @var bool Custom */
public $custom = false;
/** @var int Position */
public $position;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'feature_value',
'primary' => 'id_feature_value',
'multilang' => true,
'fields' => [
'id_feature' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'custom' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'position' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
/* Lang fields */
'value' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => FeatureValueSettings::VALUE_MAX_LENGTH],
],
];
protected $webserviceParameters = [
'objectsNodeName' => 'product_feature_values',
'objectNodeName' => 'product_feature_value',
'fields' => [
'id_feature' => ['xlink_resource' => 'product_features'],
],
];
/**
* Get all values for a given feature.
*
* @param int $idFeature Feature id
*
* @return array Array with feature's values
*/
public static function getFeatureValues($idFeature)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
'
SELECT *
FROM `' . _DB_PREFIX_ . 'feature_value`
WHERE `id_feature` = ' . (int) $idFeature
);
}
/**
* Get all values for a given feature and language.
*
* @param int $idLang Language id
* @param int $idFeature Feature id
*
* @return array Array with feature's values
*/
public static function getFeatureValuesWithLang($idLang, $idFeature, $custom = false)
{
return Db::getInstance()->executeS('
SELECT *
FROM `' . _DB_PREFIX_ . 'feature_value` v
LEFT JOIN `' . _DB_PREFIX_ . 'feature_value_lang` vl
ON (v.`id_feature_value` = vl.`id_feature_value` AND vl.`id_lang` = ' . (int) $idLang . ')
WHERE v.`id_feature` = ' . (int) $idFeature . '
' . (!$custom ? 'AND (v.`custom` IS NULL OR v.`custom` = 0)' : '') . '
ORDER BY vl.`value` ASC
');
}
/**
* Get all language for a given value.
*
* @param int $idFeatureValue Feature value id
*
* @return array Array with value's languages
*/
public static function getFeatureValueLang($idFeatureValue)
{
return Db::getInstance()->executeS('
SELECT *
FROM `' . _DB_PREFIX_ . 'feature_value_lang`
WHERE `id_feature_value` = ' . (int) $idFeatureValue . '
ORDER BY `id_lang`
');
}
/**
* Select the good lang in tab.
*
* @param array $lang Array with all language
* @param int $idLang Language id
*
* @return string String value name selected
*/
public static function selectLang($lang, $idLang)
{
foreach ($lang as $tab) {
if ($tab['id_lang'] == $idLang) {
return $tab['value'];
}
}
return '';
}
/**
* Add FeatureValue from import.
*
* @param int $idFeature
* @param string $value
* @param int|null $idProduct
* @param int|null $idLang
* @param bool $custom
*
* @return int
*/
public static function addFeatureValueImport($idFeature, $value, $idProduct = null, $idLang = null, $custom = false)
{
$idFeatureValue = false;
if (null !== $idProduct && $idProduct) {
$idFeatureValue = Db::getInstance()->getValue('
SELECT fp.`id_feature_value`
FROM ' . _DB_PREFIX_ . 'feature_product fp
INNER JOIN ' . _DB_PREFIX_ . 'feature_value fv USING (`id_feature_value`)
WHERE fp.`id_feature` = ' . (int) $idFeature . '
AND fv.`custom` = ' . (int) $custom . '
AND fp.`id_product` = ' . (int) $idProduct);
if ($custom && $idFeatureValue && null !== $idLang && $idLang) {
Db::getInstance()->execute('
UPDATE ' . _DB_PREFIX_ . 'feature_value_lang
SET `value` = \'' . pSQL($value) . '\'
WHERE `id_feature_value` = ' . (int) $idFeatureValue . '
AND `value` != \'' . pSQL($value) . '\'
AND `id_lang` = ' . (int) $idLang);
}
}
if (!$custom) {
$idFeatureValue = Db::getInstance()->getValue('
SELECT fv.`id_feature_value`
FROM ' . _DB_PREFIX_ . 'feature_value fv
LEFT JOIN ' . _DB_PREFIX_ . 'feature_value_lang fvl ON (fvl.`id_feature_value` = fv.`id_feature_value` AND fvl.`id_lang` = ' . (int) $idLang . ')
WHERE `value` = \'' . pSQL($value) . '\'
AND fv.`id_feature` = ' . (int) $idFeature . '
AND fv.`custom` = 0
GROUP BY fv.`id_feature_value`');
}
if ($idFeatureValue) {
return (int) $idFeatureValue;
}
// Feature doesn't exist, create it
$feature_value = new FeatureValue();
$feature_value->id_feature = (int) $idFeature;
$feature_value->custom = (bool) $custom;
$feature_value->value = array_fill_keys(Language::getIDs(false), $value);
$feature_value->add();
return (int) $feature_value->id;
}
/**
* Adds current FeatureValue as a new Object to the database.
*
* @param bool $autoDate Automatically set `date_upd` and `date_add` columns
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Indicates whether the FeatureValue has been successfully added
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function add($autoDate = true, $nullValues = false)
{
if ($this->position <= 0) {
$this->position = static::getHighestPosition($this->id_feature) + 1;
}
$return = parent::add($autoDate, $nullValues);
if ($return) {
Hook::exec('actionFeatureValueSave', ['id_feature_value' => $this->id]);
}
return $return;
}
/**
* Updates the current FeatureValue in the database.
*
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Indicates whether the FeatureValue has been successfully updated
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function update($nullValues = false)
{
$return = parent::update($nullValues);
if ($return) {
Hook::exec('actionFeatureValueSave', ['id_feature_value' => $this->id]);
}
return $return;
}
/**
* Deletes current FeatureValue from the database.
*
* @return bool `true` if delete was successful
*
* @throws PrestaShopException
*/
public function delete()
{
/* Also delete related products */
Db::getInstance()->execute(
'
DELETE FROM `' . _DB_PREFIX_ . 'feature_product`
WHERE `id_feature_value` = ' . (int) $this->id
);
/* Reinitializing position */
$this->cleanPositions((int) $this->id_feature);
$return = parent::delete();
if ($return) {
Hook::exec('actionFeatureValueDelete', ['id_feature_value' => $this->id]);
}
return $return;
}
/**
* Get the highest feature value position of given feature.
*
* @param int $idFeature
*
* @return int
*/
public static function getHighestPosition($idFeature)
{
$sql = 'SELECT MAX(`position`)
FROM `' . _DB_PREFIX_ . 'feature_value`
WHERE `id_feature` = ' . (int) $idFeature;
$position = Db::getInstance()->getValue($sql);
return is_numeric($position) ? $position : -1;
}
/**
* Reorder feature values within single feature.
* Use it after deleting a feature value.
*
* @param int $idFeature
* @param bool $includeCurrentFeatureValue
*
* @return bool Whether the result was successfully updated
*/
public function cleanPositions($idFeature, $includeCurrentFeatureValue = false)
{
Db::getInstance()->execute('SET @i = -1', false);
$sql = 'UPDATE `' . _DB_PREFIX_ . 'feature_value` SET `position` = @i:=@i+1 WHERE';
if (!$includeCurrentFeatureValue) {
$sql .= ' `id_feature_value` != ' . (int) $this->id . ' AND';
}
$sql .= ' `id_feature` = ' . (int) $idFeature . ' ORDER BY `position` ASC';
return Db::getInstance()->execute($sql);
}
}

84
classes/Gender.php Normal file
View File

@@ -0,0 +1,84 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Core\Domain\Title\ValueObject\Gender as ValueObjectGender;
/**
* Class GenderCore.
*/
class GenderCore extends ObjectModel
{
public const TYPE_MALE = ValueObjectGender::TYPE_MALE;
public const TYPE_FEMALE = ValueObjectGender::TYPE_FEMALE;
public const TYPE_OTHER = ValueObjectGender::TYPE_OTHER;
/** @var int|null Object ID */
public $id;
public $id_gender;
/** @var string|array<string> */
public $name;
/** @var int */
public $type;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'gender',
'primary' => 'id_gender',
'multilang' => true,
'fields' => [
'type' => ['type' => self::TYPE_INT, 'required' => true],
/* Lang fields */
'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isCatalogName', 'required' => true, 'size' => 20],
],
];
/**
* GenderCore constructor.
*
* @param int|null $id
* @param int|null $idLang
* @param int|null $idShop
*/
public function __construct($id = null, $idLang = null, $idShop = null)
{
parent::__construct($id, $idLang, $idShop);
$this->image_dir = _PS_GENDERS_DIR_;
}
/**
* Get all Genders.
*
* @param int|null $idLang Language ID
*
* @return PrestaShopCollection
*/
public static function getGenders($idLang = null)
{
if (null === $idLang) {
$idLang = Context::getContext()->language->id;
}
return new PrestaShopCollection('Gender', $idLang);
}
/**
* Get Gender image.
*
* @return string File path
*/
public function getImage()
{
if (!isset($this->id) || empty($this->id) || !file_exists(_PS_GENDERS_DIR_ . $this->id . '.jpg')) {
return _THEME_GENDERS_DIR_ . 'Unknown.jpg';
}
return _THEME_GENDERS_DIR_ . $this->id . '.jpg';
}
}

428
classes/Group.php Normal file
View File

@@ -0,0 +1,428 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
class GroupCore extends ObjectModel
{
public $id;
/** @var string|array<int, string> */
public $name;
/** @var string Reduction */
public $reduction;
/** @var int Price display method (tax inc/tax exc) */
public $price_display_method;
/** @var bool Show prices */
public $show_prices = true;
/** @var string Object creation date */
public $date_add;
/** @var string Object last modification date */
public $date_upd;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'group',
'primary' => 'id_group',
'multilang' => true,
'fields' => [
'reduction' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
'price_display_method' => ['type' => self::TYPE_INT, 'validate' => 'isPriceDisplayMethod', 'required' => true],
'show_prices' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
/* Lang fields */
'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 32],
],
];
protected static $cache_reduction = [];
protected static $group_price_display_method = [];
protected static $ps_group_feature_active = null;
protected static $groups = [];
protected static $ps_unidentified_group = null;
protected static $ps_customer_group = null;
protected $webserviceParameters = [];
public const PRICE_DISPLAY_METHOD_TAX_INCL = 0;
public const PRICE_DISPLAY_METHOD_TAX_EXCL = 1;
public function __construct($id = null, $id_lang = null, $id_shop = null)
{
parent::__construct($id, $id_lang, $id_shop);
if ($this->id && !isset(Group::$group_price_display_method[$this->id])) {
self::$group_price_display_method[$this->id] = $this->price_display_method;
}
}
/**
* WARNING: For testing only. Do NOT rely on this method, it may be removed at any time.
*/
public static function clearCachedValues()
{
self::$cache_reduction = [];
self::$group_price_display_method = [];
self::$ps_group_feature_active = null;
self::$groups = [];
self::$ps_unidentified_group = null;
self::$ps_customer_group = null;
}
public static function getGroups($id_lang, $id_shop = false)
{
$shop_criteria = '';
if ($id_shop) {
$shop_criteria = Shop::addSqlAssociation('group', 'g');
}
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT DISTINCT g.`id_group`, g.`reduction`, g.`price_display_method`, g.`show_prices`, gl.`name`
FROM `' . _DB_PREFIX_ . 'group` g
LEFT JOIN `' . _DB_PREFIX_ . 'group_lang` AS gl ON (g.`id_group` = gl.`id_group` AND gl.`id_lang` = ' . (int) $id_lang . ')
' . $shop_criteria . '
ORDER BY g.`id_group` ASC');
}
public function getCustomers($count = false, $start = 0, $limit = 0, $shop_filtering = false)
{
if ($count) {
return Db::getInstance()->getValue('
SELECT COUNT(*)
FROM `' . _DB_PREFIX_ . 'customer_group` cg
LEFT JOIN `' . _DB_PREFIX_ . 'customer` c ON (cg.`id_customer` = c.`id_customer`)
WHERE cg.`id_group` = ' . (int) $this->id . '
' . ($shop_filtering ? Shop::addSqlRestriction(Shop::SHARE_CUSTOMER) : '') . '
AND c.`deleted` != 1');
}
return Db::getInstance()->executeS('
SELECT cg.`id_customer`, c.*
FROM `' . _DB_PREFIX_ . 'customer_group` cg
LEFT JOIN `' . _DB_PREFIX_ . 'customer` c ON (cg.`id_customer` = c.`id_customer`)
WHERE cg.`id_group` = ' . (int) $this->id . '
AND c.`deleted` != 1
' . ($shop_filtering ? Shop::addSqlRestriction(Shop::SHARE_CUSTOMER) : '') . '
ORDER BY cg.`id_customer` ASC
' . ($limit > 0 ? 'LIMIT ' . (int) $start . ', ' . (int) $limit : ''));
}
public static function getReduction($id_customer = null)
{
if (!isset(self::$cache_reduction['customer'][(int) $id_customer])) {
$id_group = $id_customer ? Customer::getDefaultGroupId((int) $id_customer) : (int) Group::getCurrent()->id;
self::$cache_reduction['customer'][(int) $id_customer] = Group::getReductionByIdGroup($id_group);
}
return self::$cache_reduction['customer'][(int) $id_customer];
}
public static function getReductionByIdGroup($id_group)
{
if (!isset(self::$cache_reduction['group'][$id_group])) {
self::$cache_reduction['group'][$id_group] = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
SELECT `reduction`
FROM `' . _DB_PREFIX_ . 'group`
WHERE `id_group` = ' . (int) $id_group);
}
return self::$cache_reduction['group'][$id_group];
}
/**
* Returns price display method for a group (i.e. price should be including tax or not).
*
* @param int $id_group
*
* @return int Returns 0 (PS_TAX_INC) if tax should be included, otherwise 1 (PS_TAX_EXC) - tax should be excluded
*/
public static function getPriceDisplayMethod($id_group)
{
if (!isset(Group::$group_price_display_method[$id_group])) {
self::$group_price_display_method[$id_group] = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
'
SELECT `price_display_method`
FROM `' . _DB_PREFIX_ . 'group`
WHERE `id_group` = ' . (int) $id_group
);
}
return self::$group_price_display_method[$id_group];
}
/**
* Returns default price display method, i.e. for the 'Customers' group.
*
* @see getPriceDisplayMethod()
*
* @return int Returns 0 (PS_TAX_INC) if tax should be included, otherwise 1 (PS_TAX_EXC) - tax should be excluded
*/
public static function getDefaultPriceDisplayMethod()
{
return Group::getPriceDisplayMethod((int) Configuration::get('PS_CUSTOMER_GROUP'));
}
public function add($autodate = true, $null_values = false)
{
Configuration::updateGlobalValue('PS_GROUP_FEATURE_ACTIVE', '1');
if (parent::add($autodate, $null_values)) {
Category::setNewGroupForHome((int) $this->id);
Carrier::assignGroupToAllCarriers((int) $this->id);
return true;
}
return false;
}
public function update($autodate = true, $null_values = false)
{
if (!Configuration::getGlobalValue('PS_GROUP_FEATURE_ACTIVE') && $this->reduction > 0) {
Configuration::updateGlobalValue('PS_GROUP_FEATURE_ACTIVE', 1);
}
return parent::update($null_values);
}
public function delete()
{
// Prevent calling the logic in case of an invalid object
if (empty($this->id)) {
return false;
}
// Prevent deletion of groups currently used in configuration
if (in_array($this->id, [
(int) Configuration::get('PS_UNIDENTIFIED_GROUP'),
(int) Configuration::get('PS_GUEST_GROUP'),
(int) Configuration::get('PS_CUSTOMER_GROUP'),
])) {
throw new PrestaShopException('You cannot delete a group that is used in the shop configuration.');
}
if (parent::delete()) {
Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'cart_rule_group` WHERE `id_group` = ' . (int) $this->id);
Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customer_group` WHERE `id_group` = ' . (int) $this->id);
Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'category_group` WHERE `id_group` = ' . (int) $this->id);
Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'group_reduction` WHERE `id_group` = ' . (int) $this->id);
Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'product_group_reduction_cache` WHERE `id_group` = ' . (int) $this->id);
$this->truncateModulesRestrictions($this->id);
// Add default group (id 3) to customers without groups
Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'customer_group` (
SELECT c.id_customer, ' . (int) Configuration::get('PS_CUSTOMER_GROUP') . ' FROM `' . _DB_PREFIX_ . 'customer` c
LEFT JOIN `' . _DB_PREFIX_ . 'customer_group` cg
ON cg.id_customer = c.id_customer
WHERE cg.id_customer IS NULL)');
// Set to the customer the default group
// Select the minimal id from customer_group
Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'customer` cg
SET id_default_group =
IFNULL((
SELECT min(id_group) FROM `' . _DB_PREFIX_ . 'customer_group`
WHERE id_customer = cg.id_customer),
' . (int) Configuration::get('PS_CUSTOMER_GROUP') . ')
WHERE `id_default_group` = ' . (int) $this->id);
// Remove group restrictions
$res = Db::getInstance()->delete('module_group', 'id_group = ' . (int) $this->id);
return $res;
}
return false;
}
/**
* This method is allow to know if a feature is used or active.
*
* @return bool
*/
public static function isFeatureActive()
{
if (self::$ps_group_feature_active === null) {
self::$ps_group_feature_active = (bool) Configuration::get('PS_GROUP_FEATURE_ACTIVE');
}
return self::$ps_group_feature_active;
}
/**
* This method is allow to know if there are other groups than the default ones.
*
* @param string|null $table Name of table linked to entity
* @param bool $has_active_column True if the table has an active column
*
* @return bool
*/
public static function isCurrentlyUsed($table = null, $has_active_column = false)
{
return (bool) (Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'group`') > 3);
}
/**
* Truncate all modules restrictions for the group.
*
* @param int $id_group
*
* @return bool
*/
public static function truncateModulesRestrictions($id_group)
{
return Db::getInstance()->execute('
DELETE FROM `' . _DB_PREFIX_ . 'module_group`
WHERE `id_group` = ' . (int) $id_group);
}
/**
* Truncate all restrictions by module.
*
* @param int $id_module
*
* @return bool
*/
public static function truncateRestrictionsByModule($id_module)
{
return Db::getInstance()->execute('
DELETE FROM `' . _DB_PREFIX_ . 'module_group`
WHERE `id_module` = ' . (int) $id_module);
}
/**
* Adding restrictions modules to the group with id $id_group.
*
* @param int $id_group
* @param array $modules
* @param array $shops
*
* @return bool
*/
public static function addModulesRestrictions($id_group, $modules, $shops = [1])
{
if (!is_array($modules) || !count($modules) || !is_array($shops) || !count($shops)) {
return false;
}
// Delete all record for this group
Db::getInstance()->execute(
'DELETE FROM `' . _DB_PREFIX_ . 'module_group`
WHERE `id_group` = ' . (int) $id_group . '
AND `id_shop` IN ('
. implode(',', array_map('intval', $shops))
. ')'
);
$sql = 'INSERT INTO `' . _DB_PREFIX_ . 'module_group` (`id_module`, `id_shop`, `id_group`) VALUES ';
foreach ($modules as $module) {
foreach ($shops as $shop) {
$sql .= '("' . (int) $module . '", "' . (int) $shop . '", "' . (int) $id_group . '"),';
}
}
$sql = rtrim($sql, ',');
return (bool) Db::getInstance()->execute($sql);
}
/**
* Add restrictions for a new module.
* We authorize every groups to the new module.
*
* @param int $id_module
* @param array $shops
*
* @return bool
*/
public static function addRestrictionsForModule($id_module, $shops = [1])
{
if (!is_array($shops) || !count($shops)) {
return false;
}
$res = true;
foreach ($shops as $shop) {
$res = $res && Db::getInstance()->execute('
INSERT INTO `' . _DB_PREFIX_ . 'module_group` (`id_module`, `id_shop`, `id_group`)
(SELECT ' . (int) $id_module . ', ' . (int) $shop . ', id_group FROM `' . _DB_PREFIX_ . 'group`)');
}
return $res;
}
/**
* Return current group object
* Use context.
*
* @return Group Group object
*/
public static function getCurrent()
{
if (self::$ps_unidentified_group === null) {
self::$ps_unidentified_group = Configuration::get('PS_UNIDENTIFIED_GROUP');
}
if (self::$ps_customer_group === null) {
self::$ps_customer_group = Configuration::get('PS_CUSTOMER_GROUP');
}
$customer = Context::getContext()->customer;
if (Validate::isLoadedObject($customer)) {
$id_group = (int) $customer->id_default_group;
} else {
$id_group = (int) self::$ps_unidentified_group;
}
if (!isset(self::$groups[$id_group])) {
self::$groups[$id_group] = new Group($id_group);
}
if (!self::$groups[$id_group]->isAssociatedToShop(Context::getContext()->shop->id)) {
$id_group = (int) self::$ps_customer_group;
if (!isset(self::$groups[$id_group])) {
self::$groups[$id_group] = new Group($id_group);
}
}
return self::$groups[$id_group];
}
public static function getAllGroupIds(): array
{
$query = new DbQuery();
$query
->select('g.`id_group`')
->from('group', 'g')
->orderby('g.`id_group` ASC')
;
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query);
return array_column($result, 'id_group');
}
/**
* Light back office search for Group.
*
* @param string $query Searched string
*
* @return array Corresponding groups
*/
public static function searchByName($query)
{
return Db::getInstance()->getRow('
SELECT g.*, gl.*
FROM `' . _DB_PREFIX_ . 'group` g
LEFT JOIN `' . _DB_PREFIX_ . 'group_lang` gl
ON (g.`id_group` = gl.`id_group`)
WHERE `name` = \'' . pSQL($query) . '\'
');
}
}

229
classes/GroupReduction.php Normal file
View File

@@ -0,0 +1,229 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
class GroupReductionCore extends ObjectModel
{
public $id_group;
public $id_category;
public $reduction;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'group_reduction',
'primary' => 'id_group_reduction',
'fields' => [
'id_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_category' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'reduction' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true],
],
];
protected static $reduction_cache = [];
public function add($autodate = true, $null_values = false)
{
return parent::add($autodate, $null_values) && $this->_setCache();
}
public function update($null_values = false)
{
return parent::update($null_values) && $this->_updateCache();
}
public function delete()
{
$products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
'
SELECT cp.`id_product`
FROM `' . _DB_PREFIX_ . 'category_product` cp
WHERE cp.`id_category` = ' . (int) $this->id_category
);
$ids = array_column($products, 'id_product');
if ($ids) {
Db::getInstance()->delete('product_group_reduction_cache', 'id_product IN (' . implode(', ', $ids) . ')');
}
return parent::delete();
}
protected function _clearCache()
{
return Db::getInstance()->delete('product_group_reduction_cache', 'id_group = ' . (int) $this->id_group);
}
protected function _setCache()
{
$products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
'
SELECT cp.`id_product`
FROM `' . _DB_PREFIX_ . 'category_product` cp
WHERE cp.`id_category` = ' . (int) $this->id_category
);
$values = [];
foreach ($products as $row) {
$values[] = '(' . (int) $row['id_product'] . ', ' . (int) $this->id_group . ', ' . (float) $this->reduction . ')';
}
if (count($values)) {
$query = 'INSERT INTO `' . _DB_PREFIX_ . 'product_group_reduction_cache` (`id_product`, `id_group`, `reduction`)
VALUES ' . implode(', ', $values) . ' ON DUPLICATE KEY UPDATE
`reduction` = IF(VALUES(`reduction`) > `reduction`, VALUES(`reduction`), `reduction`)';
return Db::getInstance()->execute($query);
}
return true;
}
protected function _updateCache()
{
$products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
'
SELECT cp.`id_product`
FROM `' . _DB_PREFIX_ . 'category_product` cp
WHERE cp.`id_category` = ' . (int) $this->id_category
);
$ids = array_column($products, 'id_product');
$result = true;
if ($ids) {
$result &= Db::getInstance()->update('product_group_reduction_cache', [
'reduction' => (float) $this->reduction,
], 'id_product IN(' . implode(', ', $ids) . ') AND id_group = ' . (int) $this->id_group);
}
return $result;
}
public static function getGroupReductions($id_group, $id_lang)
{
$lang = $id_lang . Shop::addSqlRestrictionOnLang('cl');
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
'
SELECT gr.`id_group_reduction`, gr.`id_group`, gr.`id_category`, gr.`reduction`, cl.`name` AS category_name
FROM `' . _DB_PREFIX_ . 'group_reduction` gr
LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl ON (cl.`id_category` = gr.`id_category` AND cl.`id_lang` = ' . (int) $lang . ')
WHERE `id_group` = ' . (int) $id_group
);
}
public static function getValueForProduct($id_product, $id_group)
{
if (!Group::isFeatureActive()) {
return 0;
}
if (!isset(self::$reduction_cache[$id_product . '-' . $id_group])) {
self::$reduction_cache[$id_product . '-' . $id_group] = Db::getInstance()->getValue('
SELECT `reduction`
FROM `' . _DB_PREFIX_ . 'product_group_reduction_cache`
WHERE `id_product` = ' . (int) $id_product . ' AND `id_group` = ' . (int) $id_group);
}
// Should return string (decimal in database) and not a float
return self::$reduction_cache[$id_product . '-' . $id_group];
}
public static function doesExist($id_group, $id_category)
{
return (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
SELECT `id_group`
FROM `' . _DB_PREFIX_ . 'group_reduction`
WHERE `id_group` = ' . (int) $id_group . ' AND `id_category` = ' . (int) $id_category);
}
public static function getGroupsByCategoryId($id_category)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
'
SELECT gr.`id_group` as id_group, gr.`reduction` as reduction, id_group_reduction
FROM `' . _DB_PREFIX_ . 'group_reduction` gr
WHERE `id_category` = ' . (int) $id_category
);
}
public static function getGroupsReductionByCategoryId($id_category)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
'
SELECT gr.`id_group_reduction` as id_group_reduction, id_group
FROM `' . _DB_PREFIX_ . 'group_reduction` gr
WHERE `id_category` = ' . (int) $id_category
);
}
public static function setProductReduction($id_product, $id_group = null, $id_category = null, $reduction = null)
{
$res = true;
GroupReduction::deleteProductReduction((int) $id_product);
$categories = Product::getProductCategories((int) $id_product);
foreach ($categories as $category) {
$reductions = GroupReduction::getGroupsByCategoryId((int) $category);
if (!$reductions) {
continue;
}
foreach ($reductions as $reduction) {
$current_group_reduction = new GroupReduction((int) $reduction['id_group_reduction']);
$res &= $current_group_reduction->_setCache();
}
}
return $res;
}
public static function deleteProductReduction($id_product)
{
$query = 'DELETE FROM `' . _DB_PREFIX_ . 'product_group_reduction_cache` WHERE `id_product` = ' . (int) $id_product;
return Db::getInstance()->execute($query);
}
public static function duplicateReduction($id_product_old, $id_product)
{
$res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
'
SELECT pgr.`id_product`, pgr.`id_group`, pgr.`reduction`
FROM `' . _DB_PREFIX_ . 'product_group_reduction_cache` pgr
WHERE pgr.`id_product` = ' . (int) $id_product_old
);
if (!$res) {
return true;
}
$query = '';
foreach ($res as $row) {
$query .= 'INSERT INTO `' . _DB_PREFIX_ . 'product_group_reduction_cache` (`id_product`, `id_group`, `reduction`) VALUES ';
$query .= '(' . (int) $id_product . ', ' . (int) $row['id_group'] . ', ' . (float) $row['reduction'] . ') ON DUPLICATE KEY UPDATE `reduction` = ' . (float) $row['reduction'] . ';';
}
return Db::getInstance()->execute($query);
}
public static function deleteCategory($id_category)
{
$query = 'DELETE FROM `' . _DB_PREFIX_ . 'group_reduction` WHERE `id_category` = ' . (int) $id_category;
return Db::getInstance()->execute($query);
}
/**
* Reset static cache (mainly for test environment)
*/
public static function resetStaticCache()
{
static::$reduction_cache = [];
}
}

239
classes/Guest.php Normal file
View File

@@ -0,0 +1,239 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class GuestCore.
*/
class GuestCore extends ObjectModel
{
public $id_operating_system;
public $id_web_browser;
public $id_customer;
public $javascript;
public $screen_resolution_x;
public $screen_resolution_y;
public $screen_color;
public $sun_java;
public $adobe_flash;
public $adobe_director;
public $apple_quicktime;
public $real_player;
public $windows_media;
public $accept_language;
/**
* @deprecated since 9.0.0 - This functionality was disabled. Attribute will be completely removed
* in the next major. There is no replacement, all clients should have the same experience.
*
* @var bool Mobile Theme */
public $mobile_theme = false;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'guest',
'primary' => 'id_guest',
'fields' => [
'id_operating_system' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_web_browser' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_customer' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'javascript' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'screen_resolution_x' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
'screen_resolution_y' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
'screen_color' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
'sun_java' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'adobe_flash' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'adobe_director' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'apple_quicktime' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'real_player' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'windows_media' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'accept_language' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 8],
'mobile_theme' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
],
];
protected $webserviceParameters = [
'fields' => [
'id_customer' => ['xlink_resource' => 'customers'],
],
];
/**
* Set user agent.
*/
public function userAgent()
{
$userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
$acceptLanguage = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '';
$this->accept_language = $this->getLanguage($acceptLanguage);
$this->id_operating_system = $this->getOs($userAgent);
$this->id_web_browser = $this->getBrowser($userAgent);
}
/**
* Get Guest Language.
*
* @param string $acceptLanguage
*
* @return mixed|string
*/
protected function getLanguage($acceptLanguage)
{
// $langsArray is filled with all the languages accepted, ordered by priority
$langsArray = [];
preg_match_all('/([a-z]{2}(-[a-z]{2})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/', $acceptLanguage, $array);
if (count($array[1])) {
$langsArray = array_combine($array[1], $array[4]);
foreach ($langsArray as $lang => $val) {
if ($val === '') {
$langsArray[$lang] = 1;
}
}
arsort($langsArray, SORT_NUMERIC);
}
// Only the first language is returned
return count($langsArray) ? key($langsArray) : '';
}
/**
* Get browser.
*
* @param string $userAgent
*/
protected function getBrowser($userAgent)
{
$browserArray = [
'Chrome' => 'Chrome/',
'Safari' => 'Safari',
'Safari iPad' => 'iPad',
'Firefox' => 'Firefox/',
'Opera' => 'Opera',
'IE 11' => 'Trident',
'IE 10' => 'MSIE 10',
'IE 9' => 'MSIE 9',
'IE 8' => 'MSIE 8',
'IE 7' => 'MSIE 7',
'IE 6' => 'MSIE 6',
];
foreach ($browserArray as $k => $value) {
if (strstr($userAgent, $value)) {
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
SELECT `id_web_browser`
FROM `' . _DB_PREFIX_ . 'web_browser` wb
WHERE wb.`name` = \'' . pSQL($k) . '\'');
return $result['id_web_browser'] ?? null;
}
}
return null;
}
/**
* Get OS.
*
* @param string $userAgent
*/
protected function getOs($userAgent)
{
$osArray = [
'Windows 10' => 'Windows NT 10',
'Windows 8.1' => 'Windows NT 6.3',
'Windows 8' => 'Windows NT 6.2',
'Windows 7' => 'Windows NT 6.1',
'Windows Vista' => 'Windows NT 6.0',
'Windows XP' => 'Windows NT 5',
'MacOsX' => 'Mac OS X',
'Android' => 'Android',
'Linux' => 'X11',
];
foreach ($osArray as $k => $value) {
if (strstr($userAgent, $value)) {
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
SELECT `id_operating_system`
FROM `' . _DB_PREFIX_ . 'operating_system` os
WHERE os.`name` = \'' . pSQL($k) . '\'');
return $result['id_operating_system'] ?? null;
}
}
return null;
}
/**
* Get Guest ID from Customer ID.
*
* @param int $idCustomer Customer ID
*
* @return bool|int
*/
public static function getFromCustomer($idCustomer)
{
if (!Validate::isUnsignedId($idCustomer)) {
return false;
}
$result = Db::getInstance()->getRow('
SELECT `id_guest`
FROM `' . _DB_PREFIX_ . 'guest`
WHERE `id_customer` = ' . (int) $idCustomer);
return $result['id_guest'] ?? false;
}
/**
* Merge with Customer.
*
* @param int $idGuest Guest ID
* @param int $idCustomer Customer ID
*
* @return bool
*/
public function mergeWithCustomer($idGuest, $idCustomer)
{
// Since the guests are merged, the guest id in the connections table must be changed too
Db::getInstance()->update('connections', [
'id_guest' => (int) $idGuest,
], 'id_guest = ' . (int) $this->id);
// Since the guests are merged, the guest id in the cart table must be changed too
Db::getInstance()->update('cart', [
'id_guest' => (int) $idGuest,
], 'id_guest = ' . (int) $this->id);
// The existing guest is removed from the database
$existingGuest = new Guest((int) $idGuest);
$existingGuest->delete();
// The current guest is removed from the database
$this->delete();
// $this is still filled with values, so it's id is changed for the old guest
$this->id = (int) $idGuest;
$this->id_customer = (int) $idCustomer;
// $this is now the old guest but filled with the most up to date values
$this->force_id = true;
return $this->add();
}
/**
* Set new guest.
*
* @param CookieCore $cookie
*/
public static function setNewGuest($cookie)
{
$guest = new Guest(isset($cookie->id_customer) ? (int) Guest::getFromCustomer((int) $cookie->id_customer) : null);
$guest->userAgent();
$guest->save();
$cookie->id_guest = (int) $guest->id;
}
}

1600
classes/Hook.php Normal file

File diff suppressed because it is too large Load Diff

925
classes/Image.php Normal file
View File

@@ -0,0 +1,925 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Core\Exception\InvalidArgumentException;
use PrestaShop\PrestaShop\Core\Image\ImageFormatConfiguration;
/**
* Class ImageCore.
*/
class ImageCore extends ObjectModel
{
public $id;
/** @var int Image ID */
public $id_image;
/** @var int Product ID */
public $id_product;
/** @var int Position used to order images of the same product */
public $position;
/** @var bool|null Image is cover */
public $cover;
/** @var array<int,string> Legend */
public $legend;
/** @var string image extension */
public $image_format = 'jpg';
/** @var string image folder */
protected $folder;
/** @var string image path without extension */
protected $existing_path;
/** @var int access rights of created folders (octal) */
protected static $access_rights = 0775;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'image',
'primary' => 'id_image',
'multilang' => true,
'fields' => [
'id_product' => ['type' => self::TYPE_INT, 'shop' => 'both', 'validate' => 'isUnsignedId', 'required' => true],
'position' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
'cover' => ['type' => self::TYPE_BOOL, 'allow_null' => true, 'validate' => 'isBool', 'shop' => true],
'legend' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 128],
],
];
/**
* @var array
*/
protected static $_cacheGetSize = [];
/**
* ImageCore constructor.
*
* @param int|null $id
* @param int|null $idLang
* @param null $id_shop
* @param null $translator
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function __construct($id = null, $idLang = null, $id_shop = null, $translator = null)
{
parent::__construct($id, $idLang, $id_shop, $translator);
$this->image_dir = _PS_PRODUCT_IMG_DIR_;
}
/**
* Adds current Image as a new Object to the database.
*
* @param bool $autoDate Automatically set `date_upd` and `date_add` columns
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Indicates whether the Image has been successfully added
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function add($autoDate = true, $nullValues = false)
{
if ($this->position <= 0) {
$this->position = Image::getHighestPosition($this->id_product) + 1;
}
if ($this->cover) {
$this->cover = true;
} else {
$this->cover = null;
}
return parent::add($autoDate, $nullValues);
}
/**
* This override is needed because we need to set 'id_product' => (int) $this->id_product, in $data array which is
* a specific case for association between shop and image
*
* {@inheritDoc}
*/
public function associateTo($id_shops, ?int $productId = null)
{
if (!$this->id) {
return;
}
$productId = $productId ?? $this->id_product;
if (empty($productId)) {
throw new InvalidArgumentException('You cannot associate an image to shop without specifying product ID');
}
if (!is_array($id_shops)) {
$id_shops = [$id_shops];
}
$data = [];
foreach ($id_shops as $id_shop) {
if (!$this->isAssociatedToShop($id_shop)) {
$data[] = [
$this->def['primary'] => (int) $this->id,
'id_shop' => (int) $id_shop,
'id_product' => $productId,
];
}
}
if ($data) {
return Db::getInstance()->insert($this->def['table'] . '_shop', $data);
}
return true;
}
/**
* Updates the current Image in the database.
*
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Indicates whether the Image has been successfully updated
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function update($nullValues = false)
{
if ($this->cover) {
$this->cover = true;
} else {
$this->cover = null;
}
return parent::update($nullValues);
}
/**
* Deletes current Image from the database.
*
* @return bool `true` if delete was successful
*
* @throws PrestaShopException
*/
public function delete()
{
if (!parent::delete()) {
return false;
}
if ($this->hasMultishopEntries()) {
return true;
}
if (!$this->deleteProductAttributeImage() || !$this->deleteImage()) {
return false;
}
// update positions
Db::getInstance()->execute('SET @position:=0', false);
Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'image` SET position=(@position:=@position+1)
WHERE `id_product` = ' . (int) $this->id_product . ' ORDER BY position ASC');
return true;
}
/**
* Return first image (by position) associated with a product attribute.
*
* @param int $idShop Shop ID
* @param int $idLang Language ID
* @param int $idProduct Product ID
* @param int $idProductAttribute Product Attribute ID
*
* @return array
*/
public static function getBestImageAttribute($idShop, $idLang, $idProduct, $idProductAttribute)
{
$cacheId = 'Image::getBestImageAttribute-' . (int) $idProduct . '-' . (int) $idProductAttribute . '-' . (int) $idLang . '-' . (int) $idShop;
if (!Cache::isStored($cacheId)) {
$row = Db::getInstance()->getRow('
SELECT image_shop.`id_image` id_image, il.`legend`
FROM `' . _DB_PREFIX_ . 'image` i
INNER JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
ON (i.id_image = image_shop.id_image AND image_shop.id_shop = ' . (int) $idShop . ')
INNER JOIN `' . _DB_PREFIX_ . 'product_attribute_image` pai
ON (pai.`id_image` = i.`id_image` AND pai.`id_product_attribute` = ' . (int) $idProductAttribute . ')
LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il
ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $idLang . ')
WHERE i.`id_product` = ' . (int) $idProduct . ' ORDER BY i.`position` ASC');
Cache::store($cacheId, $row);
} else {
$row = Cache::retrieve($cacheId);
}
return $row;
}
/**
* Return available images for a product.
*
* @param int $idLang Language ID
* @param int $idProduct Product ID
* @param int $idProductAttribute Product Attribute ID
* @param int $idShop Shop ID
*
* @return array Images
*/
public static function getImages($idLang, $idProduct, $idProductAttribute = null, $idShop = null)
{
$attributeFilter = ($idProductAttribute ? ' AND ai.`id_product_attribute` = ' . (int) $idProductAttribute : '');
$shopFilter = ($idShop ? ' AND ims.`id_shop` = ' . (int) $idShop : '');
$sql = 'SELECT *
FROM `' . _DB_PREFIX_ . 'image` i
LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (i.`id_image` = il.`id_image`)';
if ($idProductAttribute) {
$sql .= ' LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_image` ai ON (i.`id_image` = ai.`id_image`)';
}
if ($idShop) {
$sql .= ' LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` ims ON (i.`id_image` = ims.`id_image`)';
}
$sql .= ' WHERE i.`id_product` = ' . (int) $idProduct . ' AND il.`id_lang` = ' . (int) $idLang . $attributeFilter . $shopFilter . '
ORDER BY i.`position` ASC';
return Db::getInstance()->executeS($sql);
}
/**
* Check if a product has an image available.
*
* @param int $idLang Language ID
* @param int $idProduct Product ID
* @param int $idProductAttribute Product Attribute ID
*
* @return bool
*/
public static function hasImages($idLang, $idProduct, $idProductAttribute = null)
{
$attribute_filter = ($idProductAttribute ? ' AND ai.`id_product_attribute` = ' . (int) $idProductAttribute : '');
$sql = 'SELECT 1
FROM `' . _DB_PREFIX_ . 'image` i
LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (i.`id_image` = il.`id_image`)';
if ($idProductAttribute) {
$sql .= ' LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_image` ai ON (i.`id_image` = ai.`id_image`)';
}
$sql .= ' WHERE i.`id_product` = ' . (int) $idProduct . ' AND il.`id_lang` = ' . (int) $idLang . $attribute_filter;
return (bool) Db::getInstance()->getValue($sql);
}
/**
* Return Images.
*
* @return array Images
*/
public static function getAllImages()
{
return Db::getInstance()->executeS('
SELECT `id_image`, `id_product`
FROM `' . _DB_PREFIX_ . 'image`
ORDER BY `id_image` ASC');
}
/**
* Return number of images for a product.
*
* @param int $idProduct Product ID
*
* @return int number of images
*/
public static function getImagesTotal($idProduct)
{
$result = Db::getInstance()->getRow('
SELECT COUNT(`id_image`) AS total
FROM `' . _DB_PREFIX_ . 'image`
WHERE `id_product` = ' . (int) $idProduct);
return $result['total'];
}
/**
* Return highest position of images for a product.
*
* @param int $idProduct Product ID
*
* @return int highest position of images
*/
public static function getHighestPosition($idProduct)
{
$result = Db::getInstance()->getRow('
SELECT MAX(`position`) AS max
FROM `' . _DB_PREFIX_ . 'image`
WHERE `id_product` = ' . (int) $idProduct);
return $result['max'];
}
/**
* Delete product cover.
*
* @param int $idProduct Product ID
*
* @return bool result
*/
public static function deleteCover($idProduct)
{
if (!Validate::isUnsignedId($idProduct)) {
throw new PrestaShopException('Product ID is invalid.');
}
if (file_exists(_PS_TMP_IMG_DIR_ . 'product_' . $idProduct . '.jpg')) {
unlink(_PS_TMP_IMG_DIR_ . 'product_' . $idProduct . '.jpg');
}
return Db::getInstance()->execute(
'
UPDATE `' . _DB_PREFIX_ . 'image`
SET `cover` = NULL
WHERE `id_product` = ' . (int) $idProduct
)
&& Db::getInstance()->execute(
'
UPDATE `' . _DB_PREFIX_ . 'image_shop` image_shop
SET image_shop.`cover` = NULL
WHERE image_shop.id_shop IN (' . implode(',', array_map('intval', Shop::getContextListShopID())) . ') AND image_shop.`id_product` = ' . (int) $idProduct
);
}
/**
* Get product cover.
*
* @param int $idProduct Product ID
*
* @return mixed result
*/
public static function getCover($idProduct)
{
return Db::getInstance()->getRow('
SELECT * FROM `' . _DB_PREFIX_ . 'image_shop` image_shop
WHERE image_shop.`id_product` = ' . (int) $idProduct . '
AND image_shop.`cover`= 1');
}
/**
* Get global product cover.
*
* @param int $idProduct Product ID
*
* @return mixed result
*/
public static function getGlobalCover($idProduct)
{
return Db::getInstance()->getRow('
SELECT * FROM `' . _DB_PREFIX_ . 'image` i
WHERE i.`id_product` = ' . (int) $idProduct . '
AND i.`cover`= 1');
}
/**
* Copy images from a product to another.
*
* @param int $idProductOld Source product ID
* @param int $idProductNew Destination product ID
*/
public static function duplicateProductImages($idProductOld, $idProductNew, $combinationImages)
{
$imagesTypes = ImageType::getImagesTypes('products');
$result = Db::getInstance()->executeS('
SELECT `id_image`
FROM `' . _DB_PREFIX_ . 'image`
WHERE `id_product` = ' . (int) $idProductOld);
foreach ($result as $row) {
$imageOld = new Image($row['id_image']);
$imageNew = clone $imageOld;
unset($imageNew->id);
$imageNew->id_product = (int) $idProductNew;
// A new id is generated for the cloned image when calling add()
if ($imageNew->add()) {
$newPath = $imageNew->getPathForCreation();
foreach ($imagesTypes as $imageType) {
if (file_exists(_PS_PRODUCT_IMG_DIR_ . $imageOld->getExistingImgPath() . '-' . $imageType['name'] . '.jpg')) {
$imageNew->createImgFolder();
copy(
_PS_PRODUCT_IMG_DIR_ . $imageOld->getExistingImgPath() . '-' . $imageType['name'] . '.jpg',
$newPath . '-' . $imageType['name'] . '.jpg'
);
if (Configuration::get('WATERMARK_HASH')) {
$oldImagePath = _PS_PRODUCT_IMG_DIR_ . $imageOld->getExistingImgPath() . '-' . $imageType['name'] . '-' . Configuration::get('WATERMARK_HASH') . '.jpg';
if (file_exists($oldImagePath)) {
copy($oldImagePath, $newPath . '-' . $imageType['name'] . '-' . Configuration::get('WATERMARK_HASH') . '.jpg');
}
}
}
}
if (file_exists(_PS_PRODUCT_IMG_DIR_ . $imageOld->getExistingImgPath() . '.jpg')) {
copy(_PS_PRODUCT_IMG_DIR_ . $imageOld->getExistingImgPath() . '.jpg', $newPath . '.jpg');
}
Image::replaceAttributeImageAssociationId($combinationImages, (int) $imageOld->id, (int) $imageNew->id);
// Duplicate shop associations for images
$imageNew->duplicateShops($idProductOld);
} else {
return false;
}
}
return Image::duplicateAttributeImageAssociations($combinationImages);
}
/**
* @param array $combinationImages
* @param int $savedId
* @param int $idImage
*/
protected static function replaceAttributeImageAssociationId(&$combinationImages, $savedId, $idImage)
{
if (!isset($combinationImages['new']) || !is_array($combinationImages['new'])) {
return;
}
foreach ($combinationImages['new'] as $id_product_attribute => $image_ids) {
foreach ($image_ids as $key => $imageId) {
if ((int) $imageId == (int) $savedId) {
$combinationImages['new'][$id_product_attribute][$key] = (int) $idImage;
}
}
}
}
/**
* Duplicate product attribute image associations.
*
* @param array $combinationImages
*
* @return bool
*/
public static function duplicateAttributeImageAssociations($combinationImages)
{
if (!isset($combinationImages['new']) || !is_array($combinationImages['new'])) {
return true;
}
$query = 'INSERT INTO `' . _DB_PREFIX_ . 'product_attribute_image` (`id_product_attribute`, `id_image`) VALUES ';
foreach ($combinationImages['new'] as $idProductAttribute => $imageIds) {
foreach ($imageIds as $imageId) {
$query .= '(' . (int) $idProductAttribute . ', ' . (int) $imageId . '), ';
}
}
$query = rtrim($query, ', ');
return Db::getInstance()->execute($query);
}
/**
* Change an image position and update relative positions.
*
* @param int $way position is moved up if 0, moved down if 1
* @param int $position new position of the moved image
*
* @return bool success
*/
public function updatePosition($way, $position)
{
if (!isset($this->id) || !$position) {
return false;
}
// < and > statements rather than BETWEEN operator
// since BETWEEN is treated differently according to databases
return
Db::getInstance()->execute(
'UPDATE `' . _DB_PREFIX_ . 'image`
SET `position`= `position` ' . ($way ? '- 1' : '+ 1') . '
WHERE `position`
' . ($way
? '> ' . (int) $this->position . ' AND `position` <= ' . (int) $position
: '< ' . (int) $this->position . ' AND `position` >= ' . (int) $position) . '
AND `id_product`=' . (int) $this->id_product
)
&& Db::getInstance()->execute(
'UPDATE `' . _DB_PREFIX_ . 'image`
SET `position` = ' . (int) $position . '
WHERE `id_image` = ' . (int) $this->id_image
)
;
}
/**
* @param string $type
*
* @return mixed
*/
public static function getSize($type)
{
if (!isset(self::$_cacheGetSize[$type])) {
self::$_cacheGetSize[$type] = Db::getInstance()->getRow('
SELECT `width`, `height`
FROM ' . _DB_PREFIX_ . 'image_type
WHERE `name` = \'' . pSQL($type) . '\'
');
}
return self::$_cacheGetSize[$type];
}
/**
* @param array $params
*
* @return mixed
*/
public static function getWidth($params)
{
$result = self::getSize($params['type']);
return $result['width'];
}
/**
* @param array $params
*
* @return mixed
*/
public static function getHeight($params)
{
$result = self::getSize($params['type']);
return $result['height'];
}
/**
* Clear all images in tmp dir.
*/
public static function clearTmpDir()
{
foreach (scandir(_PS_TMP_IMG_DIR_, SCANDIR_SORT_NONE) as $d) {
if (preg_match('/(.*)\.jpg$/', $d)) {
unlink(_PS_TMP_IMG_DIR_ . $d);
}
}
}
/**
* Delete Image - Product attribute associations for this image.
*/
public function deleteProductAttributeImage()
{
return Db::getInstance()->execute(
'
DELETE
FROM `' . _DB_PREFIX_ . 'product_attribute_image`
WHERE `id_image` = ' . (int) $this->id
);
}
/**
* Delete the product image from disk and remove the containing folder if empty
* Handles both legacy and new image filesystems.
*/
public function deleteImage($forceDelete = false)
{
if (!$this->id) {
return false;
}
// Delete base image
if (file_exists($this->image_dir . $this->getExistingImgPath() . '.' . $this->image_format)) {
unlink($this->image_dir . $this->getExistingImgPath() . '.' . $this->image_format);
} else {
return false;
}
$filesToDelete = [];
// Delete auto-generated images
$image_types = ImageType::getImagesTypes();
foreach ($image_types as $imageType) {
foreach (ImageFormatConfiguration::SUPPORTED_FORMATS as $imageFormat) {
$filesToDelete = $this->deleteAutoGeneratedImage($imageType, $imageFormat, $filesToDelete);
}
}
// Delete watermark image
$filesToDelete[] = $this->image_dir . $this->getExistingImgPath() . '-watermark.' . $this->image_format;
// Delete old 2x watermark image, if present
$filesToDelete[] = $this->image_dir . $this->getExistingImgPath() . '-watermark2x.' . $this->image_format;
// Delete index.php
$filesToDelete[] = $this->image_dir . $this->getImgFolder() . 'index.php';
// Delete fileType
$filesToDelete[] = $this->image_dir . $this->getImgFolder() . 'fileType';
// Delete tmp images
$filesToDelete[] = _PS_TMP_IMG_DIR_ . 'product_' . $this->id_product . '.' . $this->image_format;
$filesToDelete[] = _PS_TMP_IMG_DIR_ . 'product_mini_' . $this->id_product . '.' . $this->image_format;
foreach ($filesToDelete as $file) {
if (file_exists($file) && !@unlink($file)) {
return false;
}
}
// We need to delete old thumbnails (if exists) from variant images as well
$old_thumbnails = glob(_PS_TMP_IMG_DIR_ . 'product_mini_' . $this->id_product . '_*.' . $this->image_format);
if (!empty($old_thumbnails)) {
foreach ($old_thumbnails as $file) {
// we don't care, if it exists, because glob will handle this
@unlink($file);
}
}
// Can we delete the image folder?
if (is_dir($this->image_dir . $this->getImgFolder())) {
$deleteFolder = true;
foreach (scandir($this->image_dir . $this->getImgFolder(), SCANDIR_SORT_NONE) as $file) {
if ($file != '.' && $file != '..') {
$deleteFolder = false;
break;
}
}
}
if (isset($deleteFolder) && $deleteFolder) {
@rmdir($this->image_dir . $this->getImgFolder());
}
return true;
}
/**
* Recursively deletes all product images in the given folder tree and removes empty folders.
*
* @param string $path folder containing the product images to delete
* @param string $format image format
*
* @return bool success
*/
public static function deleteAllImages($path, $format = 'jpg')
{
if (!$path || !$format || !is_dir($path)) {
return false;
}
foreach (scandir($path, SCANDIR_SORT_NONE) as $file) {
if (preg_match('/^[0-9]+(\-(.*))?\.' . $format . '$/', $file)) {
unlink($path . $file);
} elseif (is_dir($path . $file) && preg_match('/^[0-9]$/', $file)) {
Image::deleteAllImages($path . $file . '/', $format);
}
}
// Can we remove the image folder?
if (is_numeric(basename($path))) {
$removeFolder = true;
foreach (scandir($path, SCANDIR_SORT_NONE) as $file) {
if ($file != '.' && $file != '..' && $file != 'index.php') {
$removeFolder = false;
break;
}
}
if ($removeFolder) {
// we're only removing index.php if it's a folder we want to delete
if (file_exists($path . 'index.php')) {
@unlink($path . 'index.php');
}
@rmdir($path);
}
}
return true;
}
/**
* Returns image path in the old or in the new filesystem.
*
* @ returns string image path
*/
public function getExistingImgPath()
{
if (!$this->id) {
return false;
}
if (!$this->existing_path) {
$this->existing_path = $this->getImgPath();
}
return $this->existing_path;
}
/**
* Returns the path to the folder containing the image in the new filesystem.
*
* @return string|bool path to folder
*/
public function getImgFolder()
{
if (!$this->id) {
return false;
}
if (!$this->folder) {
$this->folder = Image::getImgFolderStatic($this->id);
}
return $this->folder;
}
/**
* Create parent folders for the image in the new filesystem.
*
* @return bool success
*/
public function createImgFolder()
{
if (!$this->id) {
return false;
}
if (!file_exists(_PS_PRODUCT_IMG_DIR_ . $this->getImgFolder())) {
// Apparently sometimes mkdir cannot set the rights, and sometimes chmod can't. Trying both.
$success = @mkdir(_PS_PRODUCT_IMG_DIR_ . $this->getImgFolder(), self::$access_rights, true);
$chmod = @chmod(_PS_PRODUCT_IMG_DIR_ . $this->getImgFolder(), self::$access_rights);
}
return true;
}
/**
* Returns the path to the image without file extension.
*
* @return string|bool path
*/
public function getImgPath()
{
if (!$this->id) {
return false;
}
return $this->getImgFolder() . $this->id;
}
/**
* Returns the path to the folder containing the image in the new filesystem.
*
* @param mixed $idImage
*
* @return string|bool path to folder
*/
public static function getImgFolderStatic($idImage)
{
if (!is_numeric($idImage)) {
return false;
}
$folders = str_split((string) $idImage);
return implode('/', $folders) . '/';
}
/**
* Move all legacy product image files from the image folder root to their subfolder in the new filesystem.
* If max_execution_time is provided, stops before timeout and returns string "timeout".
* If any image cannot be moved, stops and returns "false".
*
* @param int $maxExecutionTime
*
* @return mixed success or timeout
*/
public static function moveToNewFileSystem($maxExecutionTime = 0)
{
$startTime = time();
$image = null;
$tmpFolder = 'duplicates/';
foreach (scandir(_PS_PRODUCT_IMG_DIR_, SCANDIR_SORT_NONE) as $file) {
// matches the base product image or the thumbnails
if (preg_match('/^([0-9]+\-)([0-9]+)(\-(.*))?\.jpg$/', $file, $matches)) {
// don't recreate an image object for each image type
if (!$image || $image->id !== (int) $matches[2]) {
$image = new Image((int) $matches[2]);
}
// image exists in DB and with the correct product?
if (Validate::isLoadedObject($image) && $image->id_product == (int) rtrim($matches[1], '-')) {
// create the new folder if it does not exist
if (!$image->createImgFolder()) {
return false;
}
// if there's already a file at the new image path, move it to a dump folder
// most likely the preexisting image is a demo image not linked to a product and it's ok to replace it
$newPath = _PS_PRODUCT_IMG_DIR_ . $image->getImgPath() . (isset($matches[3]) ? $matches[3] : '') . '.jpg';
if (file_exists($newPath)) {
if (!file_exists(_PS_PRODUCT_IMG_DIR_ . $tmpFolder)) {
@mkdir(_PS_PRODUCT_IMG_DIR_ . $tmpFolder, self::$access_rights);
@chmod(_PS_PRODUCT_IMG_DIR_ . $tmpFolder, self::$access_rights);
}
$tmpPath = _PS_PRODUCT_IMG_DIR_ . $tmpFolder . basename($file);
if (!@rename($newPath, $tmpPath) || !file_exists($tmpPath)) {
return false;
}
}
// move the image
if (!@rename(_PS_PRODUCT_IMG_DIR_ . $file, $newPath) || !file_exists($newPath)) {
return false;
}
}
}
if ((int) $maxExecutionTime != 0 && (time() - $startTime > (int) $maxExecutionTime - 4)) {
return 'timeout';
}
}
return true;
}
/**
* Try to create and delete some folders to check if moving images to new file system will be possible.
*
* @return bool success
*/
public static function testFileSystem()
{
$folder1 = _PS_PRODUCT_IMG_DIR_ . 'testfilesystem/';
$testFolder = $folder1 . 'testsubfolder/';
// check if folders are already existing from previous failed test
if (file_exists($testFolder)) {
@rmdir($testFolder);
@rmdir($folder1);
}
if (file_exists($testFolder)) {
return false;
}
@mkdir($testFolder, self::$access_rights, true);
@chmod($testFolder, self::$access_rights);
if (!is_writable($testFolder)) {
return false;
}
@rmdir($testFolder);
@rmdir($folder1);
if (file_exists($folder1)) {
return false;
}
return true;
}
/**
* Returns the path where a product image should be created (without file format).
*
* @return string|bool path
*/
public function getPathForCreation()
{
if (!$this->id) {
return false;
}
$path = $this->getImgPath();
$this->createImgFolder();
return _PS_PRODUCT_IMG_DIR_ . $path;
}
/**
* @param array $imageType
* @param string $imageFormat
* @param array $filesToDelete
*
* @return array
*/
private function deleteAutoGeneratedImage(array $imageType, string $imageFormat, array $filesToDelete): array
{
// Regular thumbnail
$filesToDelete[] = $this->image_dir . $this->getExistingImgPath() . '-' . $imageType['name'] . '.' . $imageFormat;
// Old 2x thumbnail, if present
$filesToDelete[] = $this->image_dir . $this->getExistingImgPath() . '-' . $imageType['name'] . '2x.' . $imageFormat;
// Watermarked thumbnail, if present
if (Configuration::get('WATERMARK_HASH')) {
$filesToDelete[] = $this->image_dir . $this->getExistingImgPath() . '-' . $imageType['name'] . '-' . Configuration::get('WATERMARK_HASH') . '.' . $imageFormat;
}
return $filesToDelete;
}
}

898
classes/ImageManager.php Normal file
View File

@@ -0,0 +1,898 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class ImageManagerCore.
*
* This class includes functions for image manipulation
*/
class ImageManagerCore
{
public const ERROR_FILE_NOT_EXIST = 1;
public const ERROR_FILE_WIDTH = 2;
public const ERROR_MEMORY_LIMIT = 3;
public const MIME_TYPE_SUPPORTED = [
'image/gif',
'image/jpg',
'image/jpeg',
'image/pjpeg',
'image/png',
'image/x-png',
'image/webp',
'image/svg+xml',
'image/svg',
];
public const EXTENSIONS_SUPPORTED = [
'gif',
'jpg',
'jpeg',
'jpe',
'png',
'webp',
];
/**
* @var array - a list of svg mime types
*/
protected const SVG_MIMETYPES = ['image/svg+xml', 'image/svg'];
/**
* Generate a cached thumbnail for object lists (eg. carrier, order statuses...etc).
*
* @param string $image Real image filename
* @param string $cacheImage Cached filename
* @param int $size Desired size
* @param string $imageType Image type
* @param bool $disableCache When turned on a timestamp will be added to the image URI to disable the HTTP cache
* @param bool $regenerate When turned on and the file already exist, the file will be regenerated
*
* @return string|bool
*/
public static function thumbnail($image, $cacheImage, $size, $imageType = 'jpg', $disableCache = true, $regenerate = false)
{
if (!file_exists($image)) {
return '';
}
if ($regenerate && file_exists(_PS_TMP_IMG_DIR_ . $cacheImage)) {
@unlink(_PS_TMP_IMG_DIR_ . $cacheImage);
}
if ($regenerate || !file_exists(_PS_TMP_IMG_DIR_ . $cacheImage)) {
$infos = getimagesize($image);
// Evaluate the memory required to resize the image: if it's too much, you can't resize it.
if (!ImageManager::checkImageMemoryLimit($image)) {
return false;
}
$x = $infos[0];
$y = $infos[1];
$maxX = $size * 3;
// Size is already ok
if ($y < $size && $x <= $maxX) {
copy($image, _PS_TMP_IMG_DIR_ . $cacheImage);
} else {
// We need to resize */
$ratioX = $x / ($y / $size);
if ($ratioX > $maxX) {
$ratioX = $maxX;
$size = $y / ($x / $maxX);
}
ImageManager::resize($image, _PS_TMP_IMG_DIR_ . $cacheImage, (int) $ratioX, (int) $size, $imageType);
}
}
return '<img src="' . self::getThumbnailPath($cacheImage, $disableCache) . '" alt="" class="imgm img-thumbnail" />';
}
/**
* @param string $cacheImage
* @param bool $disableCache
*
* @return string
*/
public static function getThumbnailPath($cacheImage, $disableCache)
{
$cacheParam = $disableCache ? '?time=' . time() : '';
$controller = Context::getContext()->controller;
if (isset($controller->controller_type) && $controller->controller_type == 'admin') {
return __PS_BASE_URI__ . 'img/tmp/' . $cacheImage . $cacheParam;
}
return _PS_TMP_IMG_ . $cacheImage . $cacheParam;
}
/**
* Check if memory limit is too long or not.
*
* @param string $image
*
* @return bool
*/
public static function checkImageMemoryLimit($image)
{
$infos = @getimagesize($image);
if (!is_array($infos) || !isset($infos['bits'])) {
return true;
}
$memoryLimit = Tools::getMemoryLimit();
// memory_limit == -1 => unlimited memory
if (function_exists('memory_get_usage') && (int) $memoryLimit != -1) {
$currentMemory = memory_get_usage();
$bits = $infos['bits'] / 8;
$channel = $infos['channels'] ?? 1;
// Evaluate the memory required to resize the image: if it's too much, you can't resize it.
// For perfs, avoid computing static maths formulas in the code. pow(2, 16) = 65536 ; 1024 * 1024 = 1048576
if (($infos[0] * $infos[1] * $bits * $channel + 65536) * 1.8 + $currentMemory > $memoryLimit - 1048576) {
return false;
}
}
return true;
}
/**
* Resize, cut and optimize image.
*
* @param string $sourceFile Image object from $_FILE
* @param string $destinationFile Destination filename
* @param int $destinationWidth Desired width (optional), pass null to use original dimensions
* @param int $destinationHeight Desired height (optional), pass null to use original dimensions
* @param string $destinationFileType Desired file type inside the image. If jpg and $forceType is false, format inside will be decided by PS_IMAGE_QUALITY
* @param bool $forceType If false and $destinationFileType is jpg, format inside will be decided by PS_IMAGE_QUALITY
* @param int $error Out error code
* @param int $targetWidth Needed by AdminImportController to speed up the import process
* @param int $targetHeight Needed by AdminImportController to speed up the import process
* @param int $quality Needed by AdminImportController to speed up the import process
* @param int $sourceWidth Needed by AdminImportController to speed up the import process
* @param int $sourceHeight Needed by AdminImportController to speed up the import process
*
* @return bool Operation result
*/
public static function resize(
$sourceFile,
$destinationFile,
$destinationWidth = null,
$destinationHeight = null,
$destinationFileType = 'jpg',
$forceType = false,
&$error = 0,
&$targetWidth = null,
&$targetHeight = null,
$quality = 5,
&$sourceWidth = null,
&$sourceHeight = null
) {
clearstatcache(true, $sourceFile);
// Check if original file exists
if (!file_exists($sourceFile) || !filesize($sourceFile)) {
$error = self::ERROR_FILE_NOT_EXIST;
return false;
}
list($tmpWidth, $tmpHeight, $sourceFileType) = getimagesize($sourceFile);
$rotate = 0;
if (function_exists('exif_read_data')) {
$exif = @exif_read_data($sourceFile);
if ($exif && isset($exif['Orientation'])) {
switch ($exif['Orientation']) {
case 3:
$sourceWidth = $tmpWidth;
$sourceHeight = $tmpHeight;
$rotate = 180;
break;
case 6:
$sourceWidth = $tmpHeight;
$sourceHeight = $tmpWidth;
$rotate = -90;
break;
case 8:
$sourceWidth = $tmpHeight;
$sourceHeight = $tmpWidth;
$rotate = 90;
break;
default:
$sourceWidth = $tmpWidth;
$sourceHeight = $tmpHeight;
}
} else {
$sourceWidth = $tmpWidth;
$sourceHeight = $tmpHeight;
}
} else {
$sourceWidth = $tmpWidth;
$sourceHeight = $tmpHeight;
}
/*
* If the filetype is not forced and we are requesting a JPG file, we will adjust the format inside
* the image according to PS_IMAGE_QUALITY in some cases.
*/
if (!$forceType && $destinationFileType === 'jpg') {
// If PS_IMAGE_QUALITY is set to png_all, we will use PNG file no matter the source.
if (Configuration::get('PS_IMAGE_QUALITY') == 'png_all') {
$destinationFileType = 'png';
}
// If PS_IMAGE_QUALITY is set to png (optional), we will use PNG if the original format could support transparency.
if (Configuration::get('PS_IMAGE_QUALITY') == 'png' && $sourceFileType != IMAGETYPE_JPEG) {
$destinationFileType = 'png';
}
}
if (!$sourceWidth) {
$error = self::ERROR_FILE_WIDTH;
return false;
}
if (!$destinationWidth) {
$destinationWidth = $sourceWidth;
}
if (!$destinationHeight) {
$destinationHeight = $sourceHeight;
}
$widthDiff = $destinationWidth / $sourceWidth;
$heightDiff = $destinationHeight / $sourceHeight;
$psImageGenerationMethod = Configuration::get('PS_IMAGE_GENERATION_METHOD');
if ($widthDiff > 1 && $heightDiff > 1) {
$nextWidth = $sourceWidth;
$nextHeight = $sourceHeight;
} else {
if ($psImageGenerationMethod == 2 || (!$psImageGenerationMethod && $widthDiff > $heightDiff)) {
$nextHeight = $destinationHeight;
$nextWidth = round(($sourceWidth * $nextHeight) / $sourceHeight);
$destinationWidth = (int) (!$psImageGenerationMethod ? $destinationWidth : $nextWidth);
} else {
$nextWidth = $destinationWidth;
$nextHeight = round($sourceHeight * $destinationWidth / $sourceWidth);
$destinationHeight = (int) (!$psImageGenerationMethod ? $destinationHeight : $nextHeight);
}
}
if (!ImageManager::checkImageMemoryLimit($sourceFile)) {
$error = self::ERROR_MEMORY_LIMIT;
return false;
}
$targetWidth = $destinationWidth;
$targetHeight = $destinationHeight;
$destImage = imagecreatetruecolor($destinationWidth, $destinationHeight);
// If the output is PNG, fill with transparency. Else fill with white background.
if (in_array($destinationFileType, ['png', 'webp', 'avif'])) {
// if png color type is 3, the file is paletted (256 colors or less). Change palette to reduce file size
if ($destinationFileType == 'png' && $sourceFileType == IMAGETYPE_PNG && self::getPNGColorType($sourceFile) == 3) {
imagetruecolortopalette($destImage, false, 255);
} else {
imagealphablending($destImage, false);
}
imagesavealpha($destImage, true);
$transparent = imagecolorallocatealpha($destImage, 255, 255, 255, 127);
imagefilledrectangle($destImage, 0, 0, $destinationWidth, $destinationHeight, $transparent);
} else {
$white = imagecolorallocate($destImage, 255, 255, 255);
imagefilledrectangle($destImage, 0, 0, $destinationWidth, $destinationHeight, $white);
}
$srcImage = ImageManager::create($sourceFileType, $sourceFile);
if ($rotate) {
/** @phpstan-ignore-next-line */
$srcImage = imagerotate($srcImage, $rotate, 0);
}
if ($destinationWidth >= $sourceWidth && $destinationHeight >= $sourceHeight) {
imagecopyresized($destImage, $srcImage, (int) (($destinationWidth - $nextWidth) / 2), (int) (($destinationHeight - $nextHeight) / 2), 0, 0, $nextWidth, $nextHeight, $sourceWidth, $sourceHeight);
} else {
ImageManager::imagecopyresampled($destImage, $srcImage, (int) (($destinationWidth - $nextWidth) / 2), (int) (($destinationHeight - $nextHeight) / 2), 0, 0, $nextWidth, $nextHeight, $sourceWidth, $sourceHeight, $quality);
}
$writeFile = ImageManager::write($destinationFileType, $destImage, $destinationFile);
Hook::exec('actionOnImageResizeAfter', ['dst_file' => $destinationFile, 'file_type' => $destinationFileType]);
@imagedestroy($srcImage);
return $writeFile;
}
/**
* @param resource|GdImage $dstImage
* @param resource|GdImage $srcImage
* @param int $dstX
* @param int $dstY
* @param int $srcX
* @param int $srcY
* @param int $dstW
* @param int $dstH
* @param int $srcW
* @param int $srcH
* @param int $quality
*
* @return bool
*/
public static function imagecopyresampled(
// @phpstan-ignore-next-line
&$dstImage,
// @phpstan-ignore-next-line
$srcImage,
$dstX,
$dstY,
$srcX,
$srcY,
$dstW,
$dstH,
$srcW,
$srcH,
$quality = 3
) {
// Plug-and-Play fastimagecopyresampled function replaces much slower imagecopyresampled.
// Just include this function and change all "imagecopyresampled" references to "fastimagecopyresampled".
// Typically from 30 to 60 times faster when reducing high resolution images down to thumbnail size using the default quality setting.
// Author: Tim Eckel - Date: 09/07/07 - Version: 1.1 - Project: FreeRingers.net - Freely distributable - These comments must remain.
//
// Optional "quality" parameter (defaults is 3). Fractional values are allowed, for example 1.5. Must be greater than zero.
// Between 0 and 1 = Fast, but mosaic results, closer to 0 increases the mosaic effect.
// 1 = Up to 350 times faster. Poor results, looks very similar to imagecopyresized.
// 2 = Up to 95 times faster. Images appear a little sharp, some prefer this over a quality of 3.
// 3 = Up to 60 times faster. Will give high quality smooth results very close to imagecopyresampled, just faster.
// 4 = Up to 25 times faster. Almost identical to imagecopyresampled for most images.
// 5 = No speedup. Just uses imagecopyresampled, no advantage over imagecopyresampled.
if ($quality <= 0) {
return false;
}
if ($quality < 5 && (($dstW * $quality) < $srcW || ($dstH * $quality) < $srcH)) {
$temp = imagecreatetruecolor($dstW * $quality + 1, $dstH * $quality + 1);
imagecopyresized($temp, $srcImage, 0, 0, $srcX, $srcY, $dstW * $quality + 1, $dstH * $quality + 1, $srcW, $srcH);
imagecopyresampled($dstImage, $temp, $dstX, $dstY, 0, 0, $dstW, $dstH, $dstW * $quality, $dstH * $quality);
imagedestroy($temp);
} else {
imagecopyresampled($dstImage, $srcImage, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH);
}
return true;
}
/**
* @param string $filename
*
* @return string|bool
*/
public static function getMimeType(string $filename)
{
$mimeType = false;
// Try with GD
if (function_exists('getimagesize')) {
$imageInfo = @getimagesize($filename);
if ($imageInfo) {
$mimeType = $imageInfo['mime'];
}
}
// Try with FileInfo
if (!$mimeType && function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $filename);
finfo_close($finfo);
}
// Try with Mime
if (!$mimeType && function_exists('mime_content_type')) {
$mimeType = mime_content_type($filename);
}
// Try with exec command and file binary
if (!$mimeType && function_exists('exec')) {
$mimeType = trim(exec('file -b --mime-type ' . escapeshellarg($filename)));
if (!$mimeType) {
$mimeType = trim(exec('file --mime ' . escapeshellarg($filename)));
}
if (!$mimeType) {
$mimeType = trim(exec('file -bi ' . escapeshellarg($filename)));
}
}
return $mimeType;
}
/**
* Check if file is a real image.
*
* @param string $filename File path to check
* @param string $fileMimeType File known mime type (generally from $_FILES)
* @param array<string>|null $mimeTypeList Allowed MIME types
*
* @return bool
*/
public static function isRealImage($filename, $fileMimeType = null, $mimeTypeList = null)
{
if (!$mimeTypeList) {
$mimeTypeList = static::MIME_TYPE_SUPPORTED;
}
$mimeType = static::getMimeType($filename);
if ($fileMimeType && (empty($mimeType) || $mimeType == 'regular file' || $mimeType == 'text/plain')) {
$mimeType = $fileMimeType;
}
// For each allowed MIME type, we are looking for it inside the current MIME type
foreach ($mimeTypeList as $type) {
if (strstr($mimeType, $type)) {
return true;
}
}
return false;
}
/**
* Check if image file extension is correct.
*
* @param string $filename Real filename
* @param array<string>|null $authorizedExtensions
*
* @return bool True if it's correct
*/
public static function isCorrectImageFileExt($filename, $authorizedExtensions = null)
{
// Filter on file extension
if (empty($authorizedExtensions)) {
$authorizedExtensions = static::EXTENSIONS_SUPPORTED;
}
$nameExplode = explode('.', $filename);
if (count($nameExplode) >= 2) {
$currentExtension = strtolower($nameExplode[count($nameExplode) - 1]);
if (!in_array($currentExtension, $authorizedExtensions)) {
return false;
}
} else {
return false;
}
return true;
}
/**
* Validate image upload (check image type and weight).
*
* @param array $file Upload $_FILE value
* @param int $maxFileSize Maximum upload size
* @param array<string>|null $types Authorized extensions
* @param array<string>|null $mimeTypeList Authorized mimetypes
*
* @return bool|string Return false if no error encountered
*/
public static function validateUpload($file, $maxFileSize = 0, $types = null, $mimeTypeList = null)
{
if ((int) $maxFileSize > 0 && $file['size'] > (int) $maxFileSize) {
return Context::getContext()->getTranslator()->trans('Image is too large (%1$d kB). Maximum allowed: %2$d kB', [$file['size'] / 1024, $maxFileSize / 1024], 'Admin.Notifications.Error');
}
if (!ImageManager::isRealImage($file['tmp_name'], $file['type'], $mimeTypeList)
|| !ImageManager::isCorrectImageFileExt($file['name'], $types)
|| preg_match('/\%00/', $file['name'])
) {
return Context::getContext()->getTranslator()->trans(
'Image format not recognized, allowed formats are: %s',
[
implode(', ', is_null($types) ? static::EXTENSIONS_SUPPORTED : $types),
],
'Admin.Notifications.Error'
);
}
if ($file['error']) {
return Context::getContext()->getTranslator()->trans('Error while uploading image; please change your server\'s settings. (Error code: %s)', [$file['error']], 'Admin.Notifications.Error');
}
return false;
}
/**
* Validate icon upload.
*
* @param array $file Upload $_FILE value
* @param int $maxFileSize Maximum upload size
*
* @return bool|string Return false if no error encountered
*/
public static function validateIconUpload($file, $maxFileSize = 0)
{
if ((int) $maxFileSize > 0 && $file['size'] > $maxFileSize) {
return Context::getContext()->getTranslator()->trans('Image is too large (%1$d kB). Maximum allowed: %2$d kB', [$file['size'] / 1000, $maxFileSize / 1000], 'Admin.Notifications.Error');
}
if (substr($file['name'], -4) != '.ico') {
return Context::getContext()->getTranslator()->trans('Image format not recognized, allowed formats are: .ico', [], 'Admin.Notifications.Error');
}
if ($file['error']) {
return Context::getContext()->getTranslator()->trans('Error while uploading image; please change your server\'s settings.', [], 'Admin.Notifications.Error');
}
return false;
}
/**
* Cut image.
*
* @param string $srcFile Origin filename
* @param string $dstFile Destination filename
* @param int $dstWidth Desired width
* @param int $dstHeight Desired height
* @param string $fileType
* @param int $dstX
* @param int $dstY
*
* @return bool Operation result
*/
public static function cut($srcFile, $dstFile, $dstWidth = null, $dstHeight = null, $fileType = 'jpg', $dstX = 0, $dstY = 0)
{
if (!file_exists($srcFile)) {
return false;
}
// Source information
$srcInfo = getimagesize($srcFile);
$src = [
'width' => $srcInfo[0],
'height' => $srcInfo[1],
'ressource' => ImageManager::create($srcInfo[2], $srcFile),
];
// Destination information
$dest = [];
$dest['x'] = $dstX;
$dest['y'] = $dstY;
$dest['width'] = null !== $dstWidth ? $dstWidth : $src['width'];
$dest['height'] = null !== $dstHeight ? $dstHeight : $src['height'];
$dest['ressource'] = ImageManager::createWhiteImage($dest['width'], $dest['height']);
$white = imagecolorallocate($dest['ressource'], 255, 255, 255);
// @phpstan-ignore-next-line
imagecopyresampled($dest['ressource'], $src['ressource'], 0, 0, $dest['x'], $dest['y'], $dest['width'], $dest['height'], $dest['width'], $dest['height']);
imagecolortransparent($dest['ressource'], $white);
$return = ImageManager::write($fileType, $dest['ressource'], $dstFile);
Hook::exec('actionOnImageCutAfter', ['dst_file' => $dstFile, 'file_type' => $fileType]);
// @phpstan-ignore-next-line
@imagedestroy($src['ressource']);
return $return;
}
/**
* Create an image with GD extension from a given type.
*
* @param int $type
* @param string $filename
*
* @return false|resource
*/
public static function create($type, $filename)
{
switch ($type) {
case IMAGETYPE_GIF:
return imagecreatefromgif($filename);
case IMAGETYPE_PNG:
return imagecreatefrompng($filename);
case IMAGETYPE_WEBP:
return imagecreatefromwebp($filename);
case IMAGETYPE_JPEG:
default:
return imagecreatefromjpeg($filename);
}
}
/**
* Create an empty image with white background.
*
* @param int $width
* @param int $height
*
* @phpstan-ignore-next-line
*
* @return resource|GdImage
*/
public static function createWhiteImage($width, $height)
{
$image = imagecreatetruecolor($width, $height);
$white = imagecolorallocate($image, 255, 255, 255);
imagefill($image, 0, 0, $white);
// @phpstan-ignore-next-line
return $image;
}
/**
* Generate and write image.
*
* @param string $type
* @param resource $resource
* @param string $filename
*
* @return bool
*/
public static function write($type, $resource, $filename)
{
static $psPngQuality = null;
static $psJpegQuality = null;
static $psWebpQuality = null;
static $psAvifQuality = null;
if ($psPngQuality === null) {
$psPngQuality = Configuration::get('PS_PNG_QUALITY');
}
if ($psJpegQuality === null) {
$psJpegQuality = Configuration::get('PS_JPEG_QUALITY');
}
if ($psWebpQuality === null) {
$psWebpQuality = Configuration::get('PS_WEBP_QUALITY');
}
if ($psAvifQuality === null) {
$psAvifQuality = Configuration::get('PS_AVIF_QUALITY');
}
$success = false;
switch ($type) {
case 'gif':
// @phpstan-ignore-next-line
$success = imagegif($resource, $filename);
break;
case 'png':
$quality = ($psPngQuality === false ? 7 : $psPngQuality);
// @phpstan-ignore-next-line
$success = imagepng($resource, $filename, (int) $quality);
break;
case 'webp':
$quality = ($psWebpQuality === false ? 80 : $psWebpQuality);
// @phpstan-ignore-next-line
$success = imagewebp($resource, $filename, (int) $quality);
break;
case 'avif':
$quality = ($psAvifQuality === false ? 80 : $psAvifQuality);
// @phpstan-ignore-next-line
$success = imageavif($resource, $filename, $quality);
break;
case 'jpg':
case 'jpeg':
default:
$quality = ($psJpegQuality === false ? 90 : $psJpegQuality);
// @phpstan-ignore-next-line
imageinterlace($resource, true); // / make it PROGRESSIVE
// @phpstan-ignore-next-line
$success = imagejpeg($resource, $filename, (int) $quality);
break;
}
// @phpstan-ignore-next-line
imagedestroy($resource);
@chmod($filename, 0664);
return $success;
}
/**
* Return the mime type by the file extension.
*
* @param string $fileName
*
* @return string
*/
public static function getMimeTypeByExtension($fileName)
{
$types = [
'image/gif' => ['gif'],
'image/jpeg' => ['jpg', 'jpeg'],
'image/png' => ['png'],
'image/webp' => ['webp'],
'image/svg+xml' => ['svg'],
'image/avif' => ['avif'],
];
$extension = substr($fileName, strrpos($fileName, '.') + 1);
$mimeType = null;
foreach ($types as $mime => $exts) {
if (in_array($extension, $exts)) {
$mimeType = $mime;
break;
}
}
if ($mimeType === null) {
$mimeType = 'image/jpeg';
}
return $mimeType;
}
public static function isSvgMimeType(string $mimeType): bool
{
return in_array($mimeType, self::SVG_MIMETYPES);
}
/**
* copyImg copy an image located in $url and save it in a path
* according to $entity->$id_entity.
*
* Calls hook actionWatermark
*
* @param int $id_entity id of product or category (set in entity)
* @param int $id_image (default null) id of the image if watermark enabled
* @param string $url path or url to use
* @param string $entity 'products' or 'categories'
* @param bool $regenerate
*
* @return bool
*/
public static function copyImg($id_entity, $id_image = null, $url = '', $entity = 'products', $regenerate = true)
{
$tmpfile = tempnam(_PS_TMP_IMG_DIR_, 'ps_import');
switch ($entity) {
default:
case 'products':
$image_obj = new Image($id_image);
$path = $image_obj->getPathForCreation();
break;
case 'categories':
$path = _PS_CAT_IMG_DIR_ . (int) $id_entity;
break;
case 'manufacturers':
$path = _PS_MANU_IMG_DIR_ . (int) $id_entity;
break;
case 'suppliers':
$path = _PS_SUPP_IMG_DIR_ . (int) $id_entity;
break;
case 'stores':
$path = _PS_STORE_IMG_DIR_ . (int) $id_entity;
break;
}
$url = urldecode(trim($url));
$parced_url = parse_url($url);
if (isset($parced_url['path'])) {
$uri = ltrim($parced_url['path'], '/');
$parts = explode('/', $uri);
foreach ($parts as &$part) {
$part = rawurlencode($part);
}
unset($part);
$parced_url['path'] = '/' . implode('/', $parts);
}
if (isset($parced_url['query'])) {
$query_parts = [];
parse_str($parced_url['query'], $query_parts);
$parced_url['query'] = http_build_query($query_parts);
}
$url = http_build_url('', $parced_url);
$orig_tmpfile = $tmpfile;
if (Tools::copy($url, $tmpfile)) {
// Evaluate the memory required to resize the image: if it's too much, you can't resize it.
if (!ImageManager::checkImageMemoryLimit($tmpfile)) {
@unlink($tmpfile);
return false;
}
$tgt_width = $tgt_height = 0;
$src_width = $src_height = 0;
$error = 0;
ImageManager::resize($tmpfile, $path . '.jpg', null, null, 'jpg', false, $error, $tgt_width, $tgt_height, 5, $src_width, $src_height);
$images_types = ImageType::getImagesTypes($entity, true);
if ($regenerate) {
$path_infos = [];
$path_infos[] = [$tgt_width, $tgt_height, $path . '.jpg'];
foreach ($images_types as $image_type) {
$tmpfile = self::get_best_path($image_type['width'], $image_type['height'], $path_infos);
if (ImageManager::resize(
$tmpfile,
$path . '-' . stripslashes($image_type['name']) . '.jpg',
$image_type['width'],
$image_type['height'],
'jpg',
false,
$error,
$tgt_width,
$tgt_height,
5,
$src_width,
$src_height
)) {
// the last image should not be added in the candidate list if it's bigger than the original image
if ($tgt_width <= $src_width && $tgt_height <= $src_height) {
$path_infos[] = [$tgt_width, $tgt_height, $path . '-' . stripslashes($image_type['name']) . '.jpg'];
}
if ($entity == 'products') {
if (is_file(_PS_TMP_IMG_DIR_ . 'product_mini_' . (int) $id_entity . '.jpg')) {
unlink(_PS_TMP_IMG_DIR_ . 'product_mini_' . (int) $id_entity . '.jpg');
}
if (is_file(_PS_TMP_IMG_DIR_ . 'product_mini_' . (int) $id_entity . '_' . (int) Context::getContext()->shop->id . '.jpg')) {
unlink(_PS_TMP_IMG_DIR_ . 'product_mini_' . (int) $id_entity . '_' . (int) Context::getContext()->shop->id . '.jpg');
}
}
}
}
Hook::exec('actionWatermark', ['id_image' => $id_image, 'id_product' => $id_entity]);
}
} else {
@unlink($orig_tmpfile);
return false;
}
unlink($orig_tmpfile);
return true;
}
public static function get_best_path($tgt_width, $tgt_height, $path_infos)
{
$path_infos = array_reverse($path_infos);
$path = '';
foreach ($path_infos as $path_info) {
list($width, $height, $path) = $path_info;
if ($width >= $tgt_width && $height >= $tgt_height) {
return $path;
}
}
return $path;
}
/**
* The function `getPNGColorType` returns the color type byte from a PNG file
*
* @param string $fileName
*
* @return int|bool
*/
public static function getPNGColorType($fileName)
{
if (!is_readable($fileName)) {
return false;
}
return ord(@file_get_contents($fileName, false, null, 25, 1));
}
}

214
classes/ImageType.php Normal file
View File

@@ -0,0 +1,214 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class ImageTypeCore.
*/
class ImageTypeCore extends ObjectModel
{
public $id;
/** @var string Name */
public $name;
/** @var int Width */
public $width;
/** @var int Height */
public $height;
/** @var bool Apply to products */
public $products;
/** @var bool Apply to categories */
public $categories;
/** @var bool Apply to manufacturers */
public $manufacturers;
/** @var bool Apply to suppliers */
public $suppliers;
/** @var bool Apply to store */
public $stores;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'image_type',
'primary' => 'id_image_type',
'fields' => [
'name' => ['type' => self::TYPE_STRING, 'validate' => 'isImageTypeName', 'required' => true, 'size' => 64],
'width' => ['type' => self::TYPE_INT, 'validate' => 'isImageSize', 'required' => true],
'height' => ['type' => self::TYPE_INT, 'validate' => 'isImageSize', 'required' => true],
'categories' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'products' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'manufacturers' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'suppliers' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'stores' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
],
];
/**
* @var array Image types cache
*/
protected static $images_types_cache = [];
protected static $images_types_name_cache = [];
protected $webserviceParameters = [];
/**
* Returns image type definitions.
*
* @param string|null $type Image type
* @param bool $orderBySize
*
* @return array Image type definitions
*
* @throws PrestaShopDatabaseException
*/
public static function getImagesTypes($type = null, $orderBySize = false)
{
if (!isset(self::$images_types_cache[$type])) {
$where = 'WHERE 1';
if (!empty($type)) {
$where .= ' AND `' . bqSQL($type) . '` = 1 ';
}
if ($orderBySize) {
$query = 'SELECT * FROM `' . _DB_PREFIX_ . 'image_type` ' . $where . ' ORDER BY `width` DESC, `height` DESC, `name`ASC';
} else {
$query = 'SELECT * FROM `' . _DB_PREFIX_ . 'image_type` ' . $where . ' ORDER BY `name` ASC';
}
self::$images_types_cache[$type] = Db::getInstance()->executeS($query);
}
return self::$images_types_cache[$type];
}
/**
* Returns image type by id.
*
* @param int $id id
*
* @return array Image type definitions
*
* @throws PrestaShopDatabaseException
*/
public static function getImageTypeById(int $id): array
{
return Db::getInstance()->getRow('SELECT * FROM `' . _DB_PREFIX_ . 'image_type` WHERE `id_image_type` = ' . $id);
}
/**
* Check if type is already registered in database.
*
* @param string $typeName Name
*
* @return int Number of results found
*/
public static function typeAlreadyExists($typeName)
{
if (!Validate::isImageTypeName($typeName)) {
throw new PrestaShopException(sprintf('"%s" is not valid image type name.', $typeName));
}
Db::getInstance()->executeS('
SELECT `id_image_type`
FROM `' . _DB_PREFIX_ . 'image_type`
WHERE `name` = \'' . pSQL($typeName) . '\'', false);
return Db::getInstance()->numRows();
}
/**
* Finds image type definition by name and type.
*
* @param string $name
* @param string $type
*/
public static function getByNameNType($name, $type = null, $order = 0)
{
static $is_passed = false;
if (!isset(self::$images_types_name_cache[$name . '_' . $type . '_' . $order]) && !$is_passed) {
$results = Db::getInstance()->executeS('SELECT * FROM `' . _DB_PREFIX_ . 'image_type`');
$types = ['products', 'categories', 'manufacturers', 'suppliers', 'stores'];
$total = count($types);
foreach ($results as $result) {
foreach ($result as $value) {
for ($i = 0; $i < $total; ++$i) {
self::$images_types_name_cache[$result['name'] . '_' . $types[$i] . '_' . $value] = $result;
}
}
}
$is_passed = true;
}
$return = false;
if (isset(self::$images_types_name_cache[$name . '_' . $type . '_' . $order])) {
$return = self::$images_types_name_cache[$name . '_' . $type . '_' . $order];
}
return $return;
}
/**
* Get formatted name.
*
* @param string $name
*
* @return string
*/
public static function getFormattedName($name)
{
$themeName = Context::getContext()->shop->theme_name;
$nameWithoutThemeName = str_replace(['_' . $themeName, $themeName . '_'], '', $name);
// check if the theme name is already in $name if yes only return $name
if ($themeName !== null && strstr($name, $themeName) && self::getByNameNType($name)) {
return $name;
}
if (self::getByNameNType($nameWithoutThemeName . '_' . $themeName)) {
return $nameWithoutThemeName . '_' . $themeName;
}
if (self::getByNameNType($themeName . '_' . $nameWithoutThemeName)) {
return $themeName . '_' . $nameWithoutThemeName;
}
// only if "default" isn't already in name, we return it with default
if (!strstr($name, 'default')) {
return $nameWithoutThemeName . '_default';
}
return $nameWithoutThemeName;
}
/**
* Get all image types.
*
* @return array
*/
public static function getAll()
{
$context = Context::getContext();
if (isset($context->shop->theme)) {
$imagesTypes = $context->shop->theme->get('image_types');
return is_array($imagesTypes) ? $imagesTypes : [];
}
return [];
}
}

1795
classes/Language.php Normal file

File diff suppressed because it is too large Load Diff

1631
classes/Link.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,572 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
use PrestaShop\PrestaShop\Core\Addon\Module\ModuleManagerBuilder;
use PrestaShop\PrestaShop\Core\Domain\Currency\Command\AddCurrencyCommand;
use PrestaShop\PrestaShop\Core\Domain\Currency\Exception\CurrencyException;
use PrestaShop\PrestaShop\Core\Domain\Currency\ValueObject\CurrencyId;
use PrestaShop\PrestaShop\Core\Localization\CLDR\LocaleRepository;
class LocalizationPackCore
{
public $name;
public $version;
protected $iso_code_lang;
protected $iso_currency;
protected $_errors = [];
/**
* Loads localization pack.
*
* @param SimpleXMLElement|string $pack Localization pack as SimpleXMLElement or plain XML string
* @param array $selection Content to import selection
* @param bool $install_mode Whether mode is installation or not
* @param string|null $iso_localization_pack Country Alpha-2 ISO code
*
* @return bool
*/
public function loadLocalisationPack($pack, $selection, $install_mode = false, $iso_localization_pack = null)
{
if ($pack instanceof SimpleXMLElement) {
$xml = $pack;
} elseif (!$xml = @simplexml_load_string($pack)) {
return false;
}
libxml_clear_errors();
$main_attributes = $xml->attributes();
$this->name = (string) $main_attributes['name'];
$this->version = (string) $main_attributes['version'];
if ($iso_localization_pack) {
$id_country = (int) Country::getByIso($iso_localization_pack);
if ($id_country) {
$country = new Country($id_country);
}
if (!$id_country || !Validate::isLoadedObject($country)) {
$this->_errors[] = Context::getContext()->getTranslator()->trans(
'Cannot load country: %d',
[$id_country],
'Admin.International.Notification'
);
return false;
}
if (!$country->active) {
$country->active = true;
if (!$country->update()) {
$this->_errors[] = Context::getContext()->getTranslator()->trans(
'Cannot enable the associated country: %s',
[$country->name],
'Admin.International.Notification'
);
}
}
}
$res = true;
if (empty($selection)) {
$res = $this->_installStates($xml);
$res = $res && $this->_installTaxes($xml);
$res = $res && $this->_installCurrencies($xml, $install_mode);
$res = $res && $this->installConfiguration($xml);
$res = $res && $this->installModules($xml);
$res = $res && $this->updateDefaultGroupDisplayMethod($xml);
if (($res || $install_mode) && isset($this->iso_code_lang)) {
if (!($id_lang = (int) Language::getIdByIso($this->iso_code_lang, true))) {
$id_lang = 1;
}
if (!$install_mode) {
Configuration::updateValue('PS_LANG_DEFAULT', $id_lang);
}
} elseif (!isset($this->iso_code_lang) && $install_mode) {
$id_lang = 1;
}
if (!empty($id_lang) && !Language::isInstalled(Language::getIsoById($id_lang))) {
$res = $res && $this->_installLanguages($xml, $install_mode);
$res = $res && $this->_installUnits($xml);
}
if ($install_mode && $res && isset($this->iso_currency)) {
Cache::clean('Currency::getIdByIsoCode_*');
$res = Configuration::updateValue('PS_CURRENCY_DEFAULT', (int) Currency::getIdByIsoCode($this->iso_currency));
Currency::refreshCurrencies();
}
} else {
foreach ($selection as $selected) {
$res = $res && Validate::isLocalizationPackSelection($selected) ? $this->{'_install' . $selected}($xml, $install_mode) : false;
}
}
return $res;
}
/**
* @param SimpleXMLElement $xml
*
* @return bool
*
* @throws PrestaShopException
*/
protected function _installStates($xml)
{
if (isset($xml->states->state)) {
foreach ($xml->states->state as $data) {
/** @var SimpleXMLElement $data */
$attributes = $data->attributes();
$id_country = ($attributes['country']) ? (int) Country::getByIso((string) $attributes['country']) : false;
$id_state = ($id_country) ? State::getIdByIso($attributes['iso_code'], $id_country) : State::getIdByName($attributes['name']);
if (!$id_state) {
$state = new State();
$state->name = (string) $attributes['name'];
$state->iso_code = (string) $attributes['iso_code'];
$state->id_country = $id_country;
$id_zone = (int) Zone::getIdByName((string) $attributes['zone']);
if (!$id_zone) {
$zone = new Zone();
$zone->name = (string) $attributes['zone'];
$zone->active = true;
if (!$zone->add()) {
$this->_errors[] = Context::getContext()->getTranslator()->trans('Invalid Zone name.', [], 'Admin.International.Notification');
return false;
}
$id_zone = $zone->id;
}
$state->id_zone = $id_zone;
if (!$state->validateFields()) {
$this->_errors[] = Context::getContext()->getTranslator()->trans('Invalid state properties.', [], 'Admin.International.Notification');
return false;
}
$country = new Country($state->id_country);
if (!$country->contains_states) {
$country->contains_states = true;
if (!$country->update()) {
$this->_errors[] = Context::getContext()->getTranslator()->trans('Cannot update the associated country: %s', [$country->name], 'Admin.International.Notification');
}
}
if (!$state->add()) {
$this->_errors[] = Context::getContext()->getTranslator()->trans('An error occurred while adding the state.', [], 'Admin.International.Notification');
return false;
}
} else {
$state = new State($id_state);
if (!Validate::isLoadedObject($state)) {
$this->_errors[] = Context::getContext()->getTranslator()->trans('An error occurred while fetching the state.', [], 'Admin.International.Notification');
return false;
}
}
}
}
return true;
}
/**
* @param SimpleXMLElement $xml
*
* @return bool
*
* @throws PrestaShopException
*/
protected function _installTaxes($xml)
{
if (isset($xml->taxes->tax)) {
$assoc_taxes = [];
foreach ($xml->taxes->tax as $taxData) {
/** @var SimpleXMLElement $taxData */
$attributes = $taxData->attributes();
if ($id_tax = Tax::getTaxIdByName($attributes['name'])) {
$assoc_taxes[(int) $attributes['id']] = $id_tax;
continue;
}
$tax = new Tax();
$tax->name[(int) Configuration::get('PS_LANG_DEFAULT')] = (string) $attributes['name'];
$tax->rate = (float) $attributes['rate'];
$tax->active = true;
if (($error = $tax->validateFields(false, true)) !== true || ($error = $tax->validateFieldsLang(false, true)) !== true) {
$this->_errors[] = Context::getContext()->getTranslator()->trans('Invalid tax properties.', [], 'Admin.International.Notification') . ' ' . $error;
return false;
}
if (!$tax->add()) {
$this->_errors[] = Context::getContext()->getTranslator()->trans('An error occurred while importing the tax: %s', [(string) $attributes['name']], 'Admin.International.Notification');
return false;
}
$assoc_taxes[(int) $attributes['id']] = $tax->id;
}
foreach ($xml->taxes->taxRulesGroup as $group) {
/** @var SimpleXMLElement $group */
$group_attributes = $group->attributes();
if (!Validate::isGenericName($group_attributes['name'])) {
continue;
}
if (TaxRulesGroup::getIdByName($group['name'])) {
continue;
}
$trg = new TaxRulesGroup();
$trg->name = $group['name'];
$trg->active = true;
if (!$trg->save()) {
$this->_errors[] = Context::getContext()->getTranslator()->trans('This tax rule cannot be saved.', [], 'Admin.International.Notification');
return false;
}
foreach ($group->taxRule as $rule) {
/** @var SimpleXMLElement $rule */
$rule_attributes = $rule->attributes();
// Validation
if (!isset($rule_attributes['iso_code_country'])) {
continue;
}
$id_country = (int) Country::getByIso(strtoupper($rule_attributes['iso_code_country']));
if (!$id_country) {
continue;
}
if (!isset($rule_attributes['id_tax']) || !array_key_exists((int) $rule_attributes['id_tax'], $assoc_taxes)) {
continue;
}
// Default values
$id_state = (int) isset($rule_attributes['iso_code_state']) ? State::getIdByIso(strtoupper($rule_attributes['iso_code_state'])) : 0;
$zipcode_from = 0;
$zipcode_to = 0;
$behavior = $rule_attributes['behavior'];
if (isset($rule_attributes['zipcode_from'])) {
$zipcode_from = $rule_attributes['zipcode_from'];
if (isset($rule_attributes['zipcode_to'])) {
$zipcode_to = $rule_attributes['zipcode_to'];
}
}
// Creation
$tr = new TaxRule();
$tr->id_tax_rules_group = $trg->id;
$tr->id_country = $id_country;
$tr->id_state = $id_state;
$tr->zipcode_from = $zipcode_from;
$tr->zipcode_to = $zipcode_to;
$tr->behavior = (string) $behavior;
$tr->description = '';
$tr->id_tax = $assoc_taxes[(int) $rule_attributes['id_tax']];
$tr->save();
}
}
}
return true;
}
/**
* @param SimpleXMLElement $xml
* @param bool $install_mode
*
* @return bool
*
* @throws PrestaShopException
*/
protected function _installCurrencies($xml, $install_mode = false)
{
if (isset($xml->currencies->currency)) {
foreach ($xml->currencies->currency as $data) {
/** @var SimpleXMLElement $data */
$attributes = $data->attributes();
if (Currency::exists($attributes['iso_code'])) {
continue;
}
$sfContainer = SymfonyContainer::getInstance();
$commandBus = $sfContainer->get('prestashop.core.command_bus');
$command = new AddCurrencyCommand(
(string) $attributes['iso_code'],
(float) 1,
true
);
/* @var CurrencyId $currencyId */
try {
$currencyId = $commandBus->handle($command);
} catch (CurrencyException $e) {
$this->_errors[] = null;
Context::getContext()->getTranslator()->trans(
'An error occurred while importing the currency: %s',
[(string) $attributes['name']],
'Admin.International.Notification'
);
return false;
}
Cache::clear();
PaymentModule::addCurrencyPermissions($currencyId->getValue());
}
$error = Currency::refreshCurrencies();
if (!empty($error)) {
$this->_errors[] = $error;
}
if (!count($this->_errors) && $install_mode && isset($attributes['iso_code']) && count($xml->currencies->currency) == 1) {
$this->iso_currency = $attributes['iso_code'];
}
}
return true;
}
/**
* @return LocaleRepository
*
* @throws Exception
*/
protected function getCldrLocaleRepository()
{
$context = Context::getContext();
$container = isset($context->controller) ? $context->controller->getContainer() : null;
if (null === $container) {
$container = SymfonyContainer::getInstance();
}
/** @var LocaleRepository $localeRepoCLDR */
$localeRepoCLDR = $container->get('prestashop.core.localization.cldr.locale_repository');
return $localeRepoCLDR;
}
/**
* @param SimpleXMLElement $xml
* @param bool $install_mode
*
* @return bool
*/
protected function _installLanguages($xml, $install_mode = false)
{
$attributes = [];
if (isset($xml->languages->language)) {
foreach ($xml->languages->language as $data) {
/** @var SimpleXMLElement $data */
$attributes = $data->attributes();
// if we are not in an installation context or if the pack is not available in the local directory
if (Language::getIdByIso($attributes['iso_code']) && !$install_mode) {
continue;
}
$freshInstall = empty(Language::getIdByIso($attributes['iso_code']));
$errors = Language::downloadAndInstallLanguagePack($attributes['iso_code'], $attributes['version'], $attributes, $freshInstall);
if ($errors !== true && is_array($errors)) {
$this->_errors = array_merge($this->_errors, $errors);
}
}
}
// change the default language if there is only one language in the localization pack
if (!count($this->_errors) && $install_mode && isset($attributes['iso_code']) && count($xml->languages->language) == 1) {
$this->iso_code_lang = $attributes['iso_code'];
}
// refreshed localized currency data
$this->refreshLocalizedCurrenciesData();
return !count($this->_errors);
}
/**
* This method aims to update localized data in currencies from CLDR reference.
* Eg currency symbol used depends on language, so it has to be updated when adding a new language
* Use-case: adding a new language should trigger all currencies update
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
* @throws PrestaShop\PrestaShop\Core\Localization\Exception\LocalizationException
*/
protected function refreshLocalizedCurrenciesData()
{
/** @var Currency[] $currencies */
$currencies = Currency::getCurrencies(true, false, true);
$languages = Language::getLanguages();
$localeRepoCLDR = $this->getCldrLocaleRepository();
foreach ($currencies as $currency) {
$currency->refreshLocalizedCurrencyData($languages, $localeRepoCLDR);
$currency->save();
}
}
/**
* @param SimpleXMLElement $xml
*
* @return bool
*/
protected function _installUnits($xml)
{
$varNames = ['weight' => 'PS_WEIGHT_UNIT', 'volume' => 'PS_VOLUME_UNIT', 'short_distance' => 'PS_DIMENSION_UNIT', 'base_distance' => 'PS_BASE_DISTANCE_UNIT', 'long_distance' => 'PS_DISTANCE_UNIT'];
if (isset($xml->units->unit)) {
foreach ($xml->units->unit as $data) {
/** @var SimpleXMLElement $data */
$attributes = $data->attributes();
if (!isset($varNames[(string) $attributes['type']])) {
$this->_errors[] = Context::getContext()->getTranslator()->trans('Localization pack corrupted: wrong unit type.', [], 'Admin.International.Notification');
return false;
}
if (!Configuration::updateValue($varNames[(string) $attributes['type']], (string) $attributes['value'])) {
$this->_errors[] = Context::getContext()->getTranslator()->trans('An error occurred while setting the units.', [], 'Admin.International.Notification');
return false;
}
}
}
return true;
}
/**
* Install/Uninstall a module from a localization file
* <modules>
* <module name="module_name" [install="0|1"] />.
*
* @param SimpleXMLElement $xml
*
* @return bool
*/
protected function installModules($xml)
{
if (isset($xml->modules)) {
foreach ($xml->modules->module as $data) {
/** @var SimpleXMLElement $data */
$attributes = $data->attributes();
$name = (string) $attributes['name'];
if ($module = Module::getInstanceByName($name)) {
$install = ($attributes['install'] == 1) ? true : false;
$moduleManagerBuilder = ModuleManagerBuilder::getInstance();
$moduleManager = $moduleManagerBuilder->build();
if ($install) {
if (!$moduleManager->isInstalled($name)) {
if (!$module->install()) {
$this->_errors[] = Context::getContext()->getTranslator()->trans('An error occurred while installing the module: %s', [$name], 'Admin.International.Notification');
}
}
} elseif ($moduleManager->isInstalled($name)) {
if (!$module->uninstall()) {
$this->_errors[] = Context::getContext()->getTranslator()->trans('An error occurred while uninstalling the module: %s', [$name], 'Admin.International.Notification');
}
}
unset($module);
} else {
$this->_errors[] = Context::getContext()->getTranslator()->trans('An error has occurred, this module does not exist: %s', [$name], 'Admin.International.Notification');
}
}
}
return true;
}
/**
* Update a configuration variable from a localization file
* <configuration>
* <configuration name="variable_name" value="variable_value" />.
*
* @param SimpleXMLElement $xml
*
* @return bool
*/
protected function installConfiguration($xml)
{
if (isset($xml->configurations)) {
foreach ($xml->configurations->configuration as $data) {
/** @var SimpleXMLElement $data */
$attributes = $data->attributes();
$name = (string) $attributes['name'];
if (isset($attributes['value']) && Configuration::get($name) !== false) {
if (!Configuration::updateValue($name, (string) $attributes['value'])) {
$this->_errors[] = Context::getContext()->getTranslator()->trans(
'An error occurred during the configuration setup: %1$s',
[$name],
'Admin.International.Notification'
);
}
}
}
}
return true;
}
/**
* @param SimpleXMLElement $xml
*
* @return bool
*/
protected function _installGroups($xml)
{
return $this->updateDefaultGroupDisplayMethod($xml);
}
/**
* @param SimpleXMLElement $xml
*
* @return bool
*/
protected function updateDefaultGroupDisplayMethod($xml)
{
if (isset($xml->group_default)) {
$attributes = $xml->group_default->attributes();
if (isset($attributes['price_display_method']) && in_array((int) $attributes['price_display_method'], [0, 1])) {
Configuration::updateValue('PRICE_DISPLAY_METHOD', (int) $attributes['price_display_method']);
foreach ([(int) Configuration::get('PS_CUSTOMER_GROUP'), (int) Configuration::get('PS_GUEST_GROUP'), (int) Configuration::get('PS_UNIDENTIFIED_GROUP')] as $id_group) {
$group = new Group((int) $id_group);
$group->price_display_method = (int) $attributes['price_display_method'];
if (!$group->save()) {
$this->_errors[] = Context::getContext()->getTranslator()->trans('An error occurred during the default group update', [], 'Admin.International.Notification');
}
}
} else {
$this->_errors[] = Context::getContext()->getTranslator()->trans('An error has occurred during the default group update', [], 'Admin.International.Notification');
}
}
return true;
}
public function getErrors()
{
return $this->_errors;
}
}

1039
classes/Mail.php Normal file

File diff suppressed because it is too large Load Diff

617
classes/Manufacturer.php Normal file
View File

@@ -0,0 +1,617 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShopBundle\Form\Admin\Type\FormattedTextareaType;
/**
* Class ManufacturerCore.
*/
class ManufacturerCore extends ObjectModel
{
public $id;
/** @var string Name */
public $name;
/** @var string|array<int, string> Description */
public $description;
/** @var string|array<int, string> Short description */
public $short_description;
/** @var int Address */
public $id_address;
/** @var string Object creation date */
public $date_add;
/** @var string Object last modification date */
public $date_upd;
/** @var string Friendly URL */
public $link_rewrite;
/** @var string|array<int, string> Meta title */
public $meta_title;
/** @var string|array<int, string> Meta description */
public $meta_description;
/** @var bool active */
public $active;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'manufacturer',
'primary' => 'id_manufacturer',
'multilang' => true,
'fields' => [
'name' => ['type' => self::TYPE_STRING, 'validate' => 'isCatalogName', 'required' => true, 'size' => 64],
'active' => ['type' => self::TYPE_BOOL],
'date_add' => ['type' => self::TYPE_DATE],
'date_upd' => ['type' => self::TYPE_DATE],
/* Lang fields */
'description' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml', 'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4],
'short_description' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml', 'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4],
'meta_title' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
'meta_description' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 512],
],
];
protected $webserviceParameters = [
'fields' => [
'active' => [],
'link_rewrite' => ['getter' => 'getLink', 'setter' => false],
],
'associations' => [
'addresses' => [
'resource' => 'address',
'setter' => false,
'fields' => [
'id' => ['xlink_resource' => 'addresses'],
],
],
],
];
/**
* ManufacturerCore constructor.
*
* @param int|null $id
* @param int|null $idLang
*/
public function __construct($id = null, $idLang = null)
{
parent::__construct($id, $idLang);
$this->link_rewrite = $this->getLink();
$this->image_dir = _PS_MANU_IMG_DIR_;
}
/**
* Deletes current Manufacturer from the database.
*
* @return bool `true` if delete was successful
*
* @throws PrestaShopException
*/
public function delete()
{
$address = new Address($this->id_address);
if (Validate::isLoadedObject($address) && !$address->delete()) {
return false;
}
if (parent::delete()) {
CartRule::cleanProductRuleIntegrity('manufacturers', $this->id);
return $this->deleteImage();
}
return false;
}
/**
* Delete several objects from database.
*
* return boolean Deletion result
*/
public function deleteSelection(array $selection)
{
$result = true;
foreach ($selection as $id) {
$this->id = (int) $id;
$this->id_address = static::getManufacturerAddress();
$result = $result && $this->delete();
}
return $result;
}
/**
* Get Manufacturer Address ID.
*
* @return bool|int
*/
protected function getManufacturerAddress()
{
if (!(int) $this->id) {
return false;
}
return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
'SELECT `id_address` FROM ' . _DB_PREFIX_ . 'address WHERE `id_manufacturer` = ' . (int) $this->id
);
}
/**
* Return manufacturers.
*
* @param bool $getNbProducts [optional] return products numbers for each
* @param int $idLang Language ID
* @param bool $active
* @param int|bool $p
* @param int|bool $n
* @param bool $allGroup
*
* @return array|bool Manufacturers
*/
public static function getManufacturers($getNbProducts = false, $idLang = 0, $active = true, $p = false, $n = false, $allGroup = false, $group_by = false, $withProduct = false)
{
if (!$idLang) {
$idLang = (int) Configuration::get('PS_LANG_DEFAULT');
}
if (!Group::isFeatureActive()) {
$allGroup = true;
}
$manufacturers = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT m.*, ml.`description`, ml.`short_description`
FROM `' . _DB_PREFIX_ . 'manufacturer` m'
. Shop::addSqlAssociation('manufacturer', 'm') .
'INNER JOIN `' . _DB_PREFIX_ . 'manufacturer_lang` ml ON (m.`id_manufacturer` = ml.`id_manufacturer` AND ml.`id_lang` = ' . (int) $idLang . ')' .
'WHERE 1 ' .
($active ? 'AND m.`active` = 1 ' : '') .
($withProduct ? 'AND m.`id_manufacturer` IN (SELECT `id_manufacturer` FROM `' . _DB_PREFIX_ . 'product`) ' : '') .
($group_by ? ' GROUP BY m.`id_manufacturer`' : '') .
'ORDER BY m.`name` ASC
' . ($p ? ' LIMIT ' . (((int) $p - 1) * (int) $n) . ',' . (int) $n : ''));
if ($manufacturers === false) {
return false;
}
if ($getNbProducts) {
$sqlGroups = '';
if (!$allGroup) {
$groups = FrontController::getCurrentCustomerGroups();
$sqlGroups = (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id);
}
$results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
'
SELECT p.`id_manufacturer`, COUNT(DISTINCT p.`id_product`) as nb_products
FROM `' . _DB_PREFIX_ . 'product` p USE INDEX (product_manufacturer)
' . Shop::addSqlAssociation('product', 'p') . '
LEFT JOIN `' . _DB_PREFIX_ . 'manufacturer` as m ON (m.`id_manufacturer`= p.`id_manufacturer`)
WHERE p.`id_manufacturer` != 0 AND product_shop.`visibility` NOT IN ("none")
' . ($active ? ' AND product_shop.`active` = 1 ' : '') . '
' . (Group::isFeatureActive() && $allGroup ? '' : ' AND EXISTS (
SELECT 1
FROM `' . _DB_PREFIX_ . 'category_group` cg
LEFT JOIN `' . _DB_PREFIX_ . 'category_product` cp ON (cp.`id_category` = cg.`id_category`)
WHERE p.`id_product` = cp.`id_product` AND cg.`id_group` ' . $sqlGroups . '
)') . '
GROUP BY p.`id_manufacturer`'
);
$counts = [];
foreach ($results as $result) {
$counts[(int) $result['id_manufacturer']] = (int) $result['nb_products'];
}
foreach ($manufacturers as $key => $manufacturer) {
if (array_key_exists((int) $manufacturer['id_manufacturer'], $counts)) {
$manufacturers[$key]['nb_products'] = $counts[(int) $manufacturer['id_manufacturer']];
} else {
$manufacturers[$key]['nb_products'] = 0;
}
}
}
$totalManufacturers = count($manufacturers);
$rewriteSettings = (int) Configuration::get('PS_REWRITING_SETTINGS');
for ($i = 0; $i < $totalManufacturers; ++$i) {
$manufacturers[$i]['link_rewrite'] = ($rewriteSettings ? Tools::str2url($manufacturers[$i]['name']) : 0);
}
return $manufacturers;
}
/**
* List of manufacturers.
*
* @param int $idLang Specify the id of the language used
*
* @return array Manufacturers lite tree
*/
public static function getLiteManufacturersList($idLang = null, $format = 'default')
{
$idLang = null === $idLang ? Context::getContext()->language->id : (int) $idLang;
$manufacturersList = [];
$manufacturers = Manufacturer::getManufacturers(false, $idLang);
if ($manufacturers && count($manufacturers)) {
foreach ($manufacturers as $manufacturer) {
if ($format === 'sitemap') {
$manufacturersList[] = [
'id' => 'manufacturer-page-' . (int) $manufacturer['id_manufacturer'],
'label' => $manufacturer['name'],
'url' => Context::getContext()->link->getManufacturerLink($manufacturer['id_manufacturer'], $manufacturer['link_rewrite']),
'children' => [],
];
} else {
$manufacturersList[] = [
'id' => (int) $manufacturer['id_manufacturer'],
'link' => Context::getContext()->link->getManufacturerLink($manufacturer['id_manufacturer'], $manufacturer['link_rewrite']),
'name' => $manufacturer['name'],
'desc' => $manufacturer['description'],
'children' => [],
];
}
}
}
return $manufacturersList;
}
/**
* Return name from id.
*
* @param int $id_manufacturer Manufacturer ID
*
* @return string name
*/
protected static $cacheName = [];
public static function getNameById($idManufacturer)
{
if (!isset(self::$cacheName[$idManufacturer])) {
self::$cacheName[$idManufacturer] = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
'
SELECT `name`
FROM `' . _DB_PREFIX_ . 'manufacturer`
WHERE `id_manufacturer` = ' . (int) $idManufacturer . '
AND `active` = 1'
);
}
return self::$cacheName[$idManufacturer];
}
/**
* Get Manufacturer ID by name.
*
* @param string $name
*
* @return bool|int
*/
public static function getIdByName($name)
{
$result = Db::getInstance()->getRow(
'
SELECT `id_manufacturer`
FROM `' . _DB_PREFIX_ . 'manufacturer`
WHERE `name` = \'' . pSQL($name) . '\''
);
if (isset($result['id_manufacturer'])) {
return (int) $result['id_manufacturer'];
}
return false;
}
/**
* Get link to Manufacturer page.
*
* @return string
*/
public function getLink()
{
return Tools::str2url($this->name);
}
/**
* Get Products by Manufacturer ID.
*
* @param int $idManufacturer
* @param int $idLang
* @param int $p
* @param int $n
* @param string|null $orderBy
* @param string|null $orderWay
* @param bool $getTotal
* @param bool $active
* @param bool $activeCategory
* @param Context|null $context
*
* @return array|bool|int
*/
public static function getProducts(
$idManufacturer,
$idLang,
$p,
$n,
$orderBy = null,
$orderWay = null,
$getTotal = false,
$active = true,
$activeCategory = true,
?Context $context = null
) {
if (!$context) {
$context = Context::getContext();
}
$front = true;
if (!in_array($context->controller->controller_type, ['front', 'modulefront'])) {
$front = false;
}
if ($p < 1) {
$p = 1;
}
if (empty($orderBy) || $orderBy == 'position') {
$orderBy = 'name';
}
if (empty($orderWay)) {
$orderWay = 'ASC';
}
if (!Validate::isOrderBy($orderBy) || !Validate::isOrderWay($orderWay)) {
throw new PrestaShopException('Invalid sorting parameters provided.');
}
$groups = FrontController::getCurrentCustomerGroups();
$sqlGroups = count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id;
/* Return only the number of products */
if ($getTotal) {
$sql = '
SELECT p.`id_product`
FROM `' . _DB_PREFIX_ . 'product` p
' . Shop::addSqlAssociation('product', 'p') . '
WHERE p.id_manufacturer = ' . (int) $idManufacturer
. ($active ? ' AND product_shop.`active` = 1' : '') . '
' . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . '
AND EXISTS (
SELECT 1
FROM `' . _DB_PREFIX_ . 'category_group` cg
LEFT JOIN `' . _DB_PREFIX_ . 'category_product` cp ON (cp.`id_category` = cg.`id_category`)' .
($activeCategory ? ' INNER JOIN `' . _DB_PREFIX_ . 'category` ca ON cp.`id_category` = ca.`id_category` AND ca.`active` = 1' : '') . '
WHERE p.`id_product` = cp.`id_product` AND cg.`id_group` ' . $sqlGroups . '
)';
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
return (int) count($result);
}
if (strpos($orderBy, '.') > 0) {
$orderBy = explode('.', $orderBy);
$orderBy = pSQL($orderBy[0]) . '.`' . pSQL($orderBy[1]) . '`';
}
if ($orderBy == 'price') {
$alias = 'product_shop.';
} elseif ($orderBy == 'name') {
$alias = 'pl.';
} elseif ($orderBy == 'manufacturer_name') {
$orderBy = 'name';
$alias = 'm.';
} elseif ($orderBy == 'quantity') {
$alias = 'stock.';
} elseif ($orderBy == 'sales') {
$alias = '';
} else {
$alias = 'p.';
}
$sql = 'SELECT p.*, product_shop.*, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity'
. (Combination::isFeatureActive() ? ', product_attribute_shop.minimal_quantity AS product_attribute_minimal_quantity, IFNULL(product_attribute_shop.`id_product_attribute`,0) id_product_attribute' : '') . '
, pl.`description`, pl.`description_short`, pl.`link_rewrite`, pl.`meta_description`,
pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`, image_shop.`id_image` id_image, il.`legend`, m.`name` AS manufacturer_name,
DATEDIFF(
product_shop.`date_add`,
DATE_SUB(
"' . date('Y-m-d') . ' 00:00:00",
INTERVAL ' . (Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20) . ' DAY
)
) > 0 AS new, psales.`quantity` as sales'
. ' FROM `' . _DB_PREFIX_ . 'product` p
' . Shop::addSqlAssociation('product', 'p') .
(Combination::isFeatureActive() ? 'LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` product_attribute_shop
ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shop.id_shop=' . (int) $context->shop->id . ')' : '') . '
LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl
ON (p.`id_product` = pl.`id_product` AND pl.`id_lang` = ' . (int) $idLang . Shop::addSqlRestrictionOnLang('pl') . ')
LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
ON (image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop=' . (int) $context->shop->id . ')
LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il
ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $idLang . ')
LEFT JOIN `' . _DB_PREFIX_ . 'product_sale` psales
ON psales.`id_product` = p.`id_product`
LEFT JOIN `' . _DB_PREFIX_ . 'manufacturer` m
ON (m.`id_manufacturer` = p.`id_manufacturer`)
' . Product::sqlStock('p', 0);
if (Group::isFeatureActive() || $activeCategory) {
$sql .= 'JOIN `' . _DB_PREFIX_ . 'category_product` cp ON (p.id_product = cp.id_product)';
if (Group::isFeatureActive()) {
$sql .= 'JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cp.`id_category` = cg.`id_category` AND cg.`id_group` ' . $sqlGroups . ')';
}
if ($activeCategory) {
$sql .= 'JOIN `' . _DB_PREFIX_ . 'category` ca ON cp.`id_category` = ca.`id_category` AND ca.`active` = 1';
}
}
$sql .= '
WHERE p.`id_manufacturer` = ' . (int) $idManufacturer . '
' . ($active ? ' AND product_shop.`active` = 1' : '') . '
' . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . '
GROUP BY p.id_product';
if ($orderBy !== 'price') {
$sql .= '
ORDER BY ' . $alias . '`' . bqSQL($orderBy) . '` ' . pSQL($orderWay) . '
LIMIT ' . (((int) $p - 1) * (int) $n) . ',' . (int) $n;
}
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
if (!$result) {
return false;
}
if ($orderBy === 'price') {
Tools::orderbyPrice($result, $orderWay);
$result = array_slice($result, (int) (($p - 1) * $n), (int) $n);
}
return $result;
}
/**
* Get Products by Manufacturer
* (light edition).
*
* @param int $idLang
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public function getProductsLite($idLang)
{
$context = Context::getContext();
$front = true;
if (!in_array($context->controller->controller_type, ['front', 'modulefront'])) {
$front = false;
}
return Db::getInstance()->executeS('
SELECT p.`id_product`, pl.`name`
FROM `' . _DB_PREFIX_ . 'product` p
' . Shop::addSqlAssociation('product', 'p') . '
LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (
p.`id_product` = pl.`id_product`
AND pl.`id_lang` = ' . (int) $idLang . $context->shop->addSqlRestrictionOnLang('pl') . '
)
WHERE p.`id_manufacturer` = ' . (int) $this->id .
($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : ''));
}
/**
* Specify if a manufacturer already in base.
*
* @param int $idManufacturer Manufacturer id
*
* @return bool
*/
public static function manufacturerExists($idManufacturer)
{
$row = Db::getInstance()->getRow(
'
SELECT `id_manufacturer`
FROM ' . _DB_PREFIX_ . 'manufacturer m
WHERE m.`id_manufacturer` = ' . (int) $idManufacturer,
false
);
return isset($row['id_manufacturer']);
}
/**
* Get Manufacturer Addresses.
*
* @param int $idLang
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public function getAddresses($idLang)
{
return Db::getInstance()->executeS(
'
SELECT a.*, cl.name AS `country`, s.name AS `state`
FROM `' . _DB_PREFIX_ . 'address` AS a
LEFT JOIN `' . _DB_PREFIX_ . 'country_lang` AS cl ON (
cl.`id_country` = a.`id_country`
AND cl.`id_lang` = ' . (int) $idLang . '
)
LEFT JOIN `' . _DB_PREFIX_ . 'state` AS s ON (s.`id_state` = a.`id_state`)
WHERE `id_manufacturer` = ' . (int) $this->id . '
AND a.`deleted` = 0'
);
}
/**
* Get Manufacturer Addresses
* (for webservice).
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public function getWsAddresses()
{
return Db::getInstance()->executeS(
'
SELECT a.id_address as id
FROM `' . _DB_PREFIX_ . 'address` AS a
' . Shop::addSqlAssociation('manufacturer', 'a') . '
WHERE a.`id_manufacturer` = ' . (int) $this->id . '
AND a.`deleted` = 0'
);
}
/**
* Set Manufacturer Addresses
* (for webservice).
*
* @param array $idAddresses
*
* @return bool
*/
public function setWsAddresses($idAddresses)
{
$ids = [];
foreach ($idAddresses as $id) {
$ids[] = (int) $id['id'];
}
$result1 = (
Db::getInstance()->execute('
UPDATE `' . _DB_PREFIX_ . 'address`
SET id_manufacturer = 0
WHERE id_manufacturer = ' . (int) $this->id . '
AND deleted = 0') !== false
);
$result2 = true;
if (count($ids)) {
$result2 = (
Db::getInstance()->execute('
UPDATE `' . _DB_PREFIX_ . 'address`
SET id_customer = 0, id_supplier = 0, id_manufacturer = ' . (int) $this->id . '
WHERE id_address IN(' . implode(',', $ids) . ')
AND deleted = 0') !== false
);
}
return $result1 && $result2;
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class ManufacturerAddressCore.
*
* Holds address info of a Manufacturer.
* This class extends AddressCore to be differentiated from other AddressCore objects in DB.
*/
class ManufacturerAddressCore extends AddressCore
{
}

363
classes/Media.php Normal file
View File

@@ -0,0 +1,363 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class MediaCore.
*/
class MediaCore
{
public static $jquery_ui_dependencies = [
'ui.core' => ['fileName' => 'jquery.ui.core.min.js', 'dependencies' => [], 'theme' => true],
'ui.widget' => ['fileName' => 'jquery.ui.widget.min.js', 'dependencies' => [], 'theme' => false],
'ui.mouse' => ['fileName' => 'jquery.ui.mouse.min.js', 'dependencies' => ['ui.core', 'ui.widget'], 'theme' => false],
'ui.position' => ['fileName' => 'jquery.ui.position.min.js', 'dependencies' => [], 'theme' => false],
'ui.draggable' => ['fileName' => 'jquery.ui.draggable.min.js', 'dependencies' => ['ui.core', 'ui.widget', 'ui.mouse'], 'theme' => false],
'ui.droppable' => ['fileName' => 'jquery.ui.droppable.min.js', 'dependencies' => ['ui.core', 'ui.widget', 'ui.mouse', 'ui.draggable'], 'theme' => false],
'ui.resizable' => ['fileName' => 'jquery.ui.resizable.min.js', 'dependencies' => ['ui.core', 'ui.widget', 'ui.mouse'], 'theme' => true],
'ui.selectable' => ['fileName' => 'jquery.ui.selectable.min.js', 'dependencies' => ['ui.core', 'ui.widget', 'ui.mouse'], 'theme' => true],
'ui.sortable' => ['fileName' => 'jquery.ui.sortable.min.js', 'dependencies' => ['ui.core', 'ui.widget', 'ui.mouse'], 'theme' => true],
'ui.autocomplete' => ['fileName' => 'jquery.ui.autocomplete.min.js', 'dependencies' => ['ui.core', 'ui.widget', 'ui.position', 'ui.menu'], 'theme' => true],
'ui.button' => ['fileName' => 'jquery.ui.button.min.js', 'dependencies' => ['ui.core', 'ui.widget'], 'theme' => true],
'ui.dialog' => ['fileName' => 'jquery.ui.dialog.min.js', 'dependencies' => ['ui.core', 'ui.widget', 'ui.position', 'ui.button'], 'theme' => true],
'ui.menu' => ['fileName' => 'jquery.ui.menu.min.js', 'dependencies' => ['ui.core', 'ui.widget', 'ui.position'], 'theme' => true],
'ui.slider' => ['fileName' => 'jquery.ui.slider.min.js', 'dependencies' => ['ui.core', 'ui.widget', 'ui.mouse'], 'theme' => true],
'ui.spinner' => ['fileName' => 'jquery.ui.spinner.min.js', 'dependencies' => ['ui.core', 'ui.widget', 'ui.button'], 'theme' => true],
'ui.tabs' => ['fileName' => 'jquery.ui.tabs.min.js', 'dependencies' => ['ui.core', 'ui.widget'], 'theme' => true],
'ui.datepicker' => ['fileName' => 'jquery.ui.datepicker.min.js', 'dependencies' => ['ui.core'], 'theme' => true],
'ui.progressbar' => ['fileName' => 'jquery.ui.progressbar.min.js', 'dependencies' => ['ui.core', 'ui.widget'], 'theme' => true],
'ui.tooltip' => ['fileName' => 'jquery.ui.tooltip.min.js', 'dependencies' => ['ui.core', 'ui.widget', 'ui.position', 'effects.core'], 'theme' => true],
'ui.accordion' => ['fileName' => 'jquery.ui.accordion.min.js', 'dependencies' => ['ui.core', 'ui.widget', 'effects.core'], 'theme' => true],
'effects.core' => ['fileName' => 'jquery.effects.core.min.js', 'dependencies' => [], 'theme' => false],
'effects.blind' => ['fileName' => 'jquery.effects.blind.min.js', 'dependencies' => ['effects.core'], 'theme' => false],
'effects.bounce' => ['fileName' => 'jquery.effects.bounce.min.js', 'dependencies' => ['effects.core'], 'theme' => false],
'effects.clip' => ['fileName' => 'jquery.effects.clip.min.js', 'dependencies' => ['effects.core'], 'theme' => false],
'effects.drop' => ['fileName' => 'jquery.effects.drop.min.js', 'dependencies' => ['effects.core'], 'theme' => false],
'effects.explode' => ['fileName' => 'jquery.effects.explode.min.js', 'dependencies' => ['effects.core'], 'theme' => false],
'effects.fade' => ['fileName' => 'jquery.effects.fade.min.js', 'dependencies' => ['effects.core'], 'theme' => false],
'effects.fold' => ['fileName' => 'jquery.effects.fold.min.js', 'dependencies' => ['effects.core'], 'theme' => false],
'effects.highlight' => ['fileName' => 'jquery.effects.highlight.min.js', 'dependencies' => ['effects.core'], 'theme' => false],
'effects.pulsate' => ['fileName' => 'jquery.effects.pulsate.min.js', 'dependencies' => ['effects.core'], 'theme' => false],
'effects.scale' => ['fileName' => 'jquery.effects.scale.min.js', 'dependencies' => ['effects.core'], 'theme' => false],
'effects.shake' => ['fileName' => 'jquery.effects.shake.min.js', 'dependencies' => ['effects.core'], 'theme' => false],
'effects.slide' => ['fileName' => 'jquery.effects.slide.min.js', 'dependencies' => ['effects.core'], 'theme' => false],
'effects.transfer' => ['fileName' => 'jquery.effects.transfer.min.js', 'dependencies' => ['effects.core'], 'theme' => false],
];
private static $jquery_ui_datepicker_iso_code = [
'bn' => 'en',
'bz' => 'en',
'dh' => 'de',
'gb' => 'en-GB',
'ag' => 'es',
'cb' => 'es',
'mx' => 'es',
'pe' => 'es',
've' => 'es',
'qc' => 'fr-CA',
'ga' => 'en',
'lo' => 'en',
'br' => 'pt-BR',
'sh' => 'en',
'si' => 'sl',
'ug' => 'en',
'ur' => 'en',
'vn' => 'vi',
'zh' => 'zh-CN',
'tw' => 'zh-TW',
];
/**
* @var array list of javascript definitions
*/
protected static $js_def = [];
/**
* addJS return javascript path.
*
* @param mixed $jsUri
*
* @return string
*/
public static function getJSPath($jsUri)
{
return Media::getMediaPath($jsUri);
}
/**
* addCSS return stylesheet path.
*
* @param mixed $cssUri
* @param string $cssMediaType
* @param bool $needRtl
*
* @return bool|array<string, string>
*/
public static function getCSSPath($cssUri, $cssMediaType = 'all', $needRtl = true)
{
// RTL Ready: search and load rtl css file if it's not originally rtl
if ($needRtl && Context::getContext()->language->is_rtl) {
$cssUriRtl = preg_replace('/(^[^.].*)(\.css)$/', '$1_rtl.css', $cssUri);
$rtlMedia = Media::getMediaPath($cssUriRtl, $cssMediaType);
if ($rtlMedia != false) {
return $rtlMedia;
}
}
// End RTL
return Media::getMediaPath($cssUri, $cssMediaType);
}
/**
* Get Media path.
*
* @param array|string|null $mediaUri
* @param string|null $cssMediaType
*
* @return bool|string|array<string, string>
*/
public static function getMediaPath($mediaUri, $cssMediaType = null)
{
if (is_array($mediaUri) || $mediaUri === null || empty($mediaUri)) {
return false;
}
$urlData = parse_url($mediaUri);
if (!is_array($urlData) || !array_key_exists('path', $urlData)) {
return false;
}
if (!array_key_exists('host', $urlData)) {
$mediaUri = '/' . ltrim(str_replace(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, _PS_ROOT_DIR_), __PS_BASE_URI__, $urlData['path']), '/\\');
// remove PS_BASE_URI on _PS_ROOT_DIR_ for the following
$fileUri = _PS_ROOT_DIR_ . Tools::str_replace_once(__PS_BASE_URI__, DIRECTORY_SEPARATOR, $mediaUri);
if (!file_exists($fileUri) || !@filemtime($fileUri) || @filesize($fileUri) === 0) {
return false;
}
$mediaUri = str_replace('//', '/', $mediaUri);
if (array_key_exists('query', $urlData)) {
$mediaUri .= '?' . $urlData['query'];
}
}
if ($cssMediaType) {
return [$mediaUri => $cssMediaType];
}
return $mediaUri;
}
/**
* return jqueryUI component path.
*
* @param string $component
* @param string $theme
* @param bool $checkDependencies
*
* @return array<string, array<string>>
*/
public static function getJqueryUIPath($component, $theme, $checkDependencies)
{
$uiPath = ['js' => [], 'css' => []];
$folder = _PS_JS_DIR_ . 'jquery/ui/';
$file = 'jquery.' . $component . '.min.js';
$urlData = parse_url($folder . $file);
$fileUri = _PS_ROOT_DIR_ . Tools::str_replace_once(__PS_BASE_URI__, DIRECTORY_SEPARATOR, $urlData['path']);
$uiTmp = [];
if (isset(Media::$jquery_ui_dependencies[$component]) && Media::$jquery_ui_dependencies[$component]['theme'] && $checkDependencies) {
$themeCss = Media::getCSSPath($folder . 'themes/' . $theme . '/jquery.ui.theme.css');
$compCss = Media::getCSSPath($folder . 'themes/' . $theme . '/jquery.' . $component . '.css');
if (!empty($themeCss)) {
$uiPath['css'] = array_merge($uiPath['css'], $themeCss);
}
if (!empty($compCss)) {
$uiPath['css'] = array_merge($uiPath['css'], $compCss);
}
}
if ($checkDependencies && array_key_exists($component, self::$jquery_ui_dependencies)) {
foreach (self::$jquery_ui_dependencies[$component]['dependencies'] as $dependency) {
$uiTmp[] = Media::getJqueryUIPath($dependency, $theme, false);
if (self::$jquery_ui_dependencies[$dependency]['theme']) {
$depCss = Media::getCSSPath($folder . 'themes/' . $theme . '/jquery.' . $dependency . '.css');
}
if (isset($depCss) && !empty($depCss)) {
$uiPath['css'] = array_merge($uiPath['css'], $depCss);
}
}
}
if (@filemtime($fileUri)) {
if (!empty($uiTmp)) {
foreach ($uiTmp as $ui) {
if (!empty($ui['js'][0])) {
$uiPath['js'][] = $ui['js'][0];
}
if (!empty($ui['css'][0])) {
$uiPath['css'][] = $ui['css'][0];
}
}
}
$uiPath['js'][] = Media::getJSPath($folder . $file);
}
// add i18n file for datepicker
if ($component == 'ui.datepicker') {
$datePickerIsoCode = Context::getContext()->language->iso_code;
if (array_key_exists($datePickerIsoCode, self::$jquery_ui_datepicker_iso_code)) {
$datePickerIsoCode = self::$jquery_ui_datepicker_iso_code[$datePickerIsoCode];
}
$uiPath['js'][] = Media::getJSPath($folder . 'i18n/jquery.ui.datepicker-' . $datePickerIsoCode . '.js');
}
return $uiPath;
}
/**
* return jquery plugin path.
*
* @param mixed $name
* @param string|null $folder
*
* @return bool|array{js: string, css: array<string, string>}
*/
public static function getJqueryPluginPath($name, $folder = null)
{
$pluginPath = ['js' => [], 'css' => []];
if ($folder === null) {
$folder = _PS_JS_DIR_ . 'jquery/plugins/';
} // set default folder
$file = 'jquery.' . $name . '.js';
$urlData = parse_url($folder);
$fileUri = _PS_ROOT_DIR_ . Tools::str_replace_once(__PS_BASE_URI__, DIRECTORY_SEPARATOR, $urlData['path']);
if (@file_exists($fileUri . $file)) {
$pluginPath['js'] = Media::getJSPath($folder . $file);
} elseif (@file_exists($fileUri . $name . '/' . $file)) {
$pluginPath['js'] = Media::getJSPath($folder . $name . '/' . $file);
} else {
return false;
}
$pluginPath['css'] = Media::getJqueryPluginCSSPath($name, $folder);
return $pluginPath;
}
/**
* return jquery plugin css path if exist.
*
* @param mixed $name
* @param string|null $folder
*
* @return bool|array<string, string>
*/
public static function getJqueryPluginCSSPath($name, $folder = null)
{
if ($folder === null) {
$folder = _PS_JS_DIR_ . 'jquery/plugins/';
} // set default folder
$file = 'jquery.' . $name . '.css';
$urlData = parse_url($folder);
$fileUri = _PS_ROOT_DIR_ . Tools::str_replace_once(__PS_BASE_URI__, DIRECTORY_SEPARATOR, $urlData['path']);
if (@file_exists($fileUri . $file)) {
return Media::getCSSPath($folder . $file);
} elseif (@file_exists($fileUri . $name . '/' . $file)) {
return Media::getCSSPath($folder . $name . '/' . $file);
} else {
return false;
}
}
/**
* Clear theme cache.
*/
public static function clearCache()
{
$files = array_merge(
glob(_PS_THEME_DIR_ . 'assets/cache/*', GLOB_NOSORT),
glob(_PS_THEME_DIR_ . 'cache/*', GLOB_NOSORT)
);
foreach ($files as $file) {
if ('index.php' !== basename($file)) {
Tools::deleteFile($file);
}
}
$version = (int) Configuration::get('PS_CCCJS_VERSION');
Configuration::updateValue('PS_CCCJS_VERSION', ++$version);
$version = (int) Configuration::get('PS_CCCCSS_VERSION');
Configuration::updateValue('PS_CCCCSS_VERSION', ++$version);
if (Shop::getContext() != Shop::CONTEXT_SHOP) {
foreach (Shop::getShops() as $shop) {
if (Configuration::hasKey('PS_CCCJS_VERSION', null, null, (int) $shop['id_shop'])) {
$version = (int) Configuration::get('PS_CCCJS_VERSION', null, null, (int) $shop['id_shop']);
Configuration::updateValue('PS_CCCJS_VERSION', ++$version, false, null, (int) $shop['id_shop']);
}
if (Configuration::hasKey('PS_CCCCSS_VERSION', null, null, (int) $shop['id_shop'])) {
$version = (int) Configuration::get('PS_CCCCSS_VERSION', null, null, (int) $shop['id_shop']);
Configuration::updateValue('PS_CCCCSS_VERSION', ++$version, false, null, (int) $shop['id_shop']);
}
}
}
}
/**
* Get JS definitions.
*
* @return array JS definitions
*/
public static function getJsDef()
{
ksort(Media::$js_def);
return Media::$js_def;
}
/**
* Add a new javascript definition at bottom of page.
*
* @param mixed $jsDef
*/
public static function addJsDef($jsDef)
{
if (is_array($jsDef)) {
foreach ($jsDef as $key => $js) {
Media::$js_def[$key] = $js;
}
} elseif ($jsDef) {
Media::$js_def[] = $jsDef;
}
}
/**
* Add a new javascript definition from a capture at bottom of page.
*
* @param mixed $params
* @param string $content
* @param Smarty $smarty
* @param bool $repeat
*/
public static function addJsDefL($params, $content, $smarty = null, &$repeat = false)
{
if (!$repeat && isset($params) && Tools::strlen($content)) {
if (!is_array($params)) {
$params = (array) $params;
}
foreach ($params as $param) {
Media::$js_def[$param] = $content;
}
}
}
}

171
classes/Message.php Normal file
View File

@@ -0,0 +1,171 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShopBundle\Form\Admin\Type\FormattedTextareaType;
/**
* Class MessageCore.
*/
class MessageCore extends ObjectModel
{
public $id;
/** @var string message content */
public $message;
/** @var int Cart ID (if applicable) */
public $id_cart;
/** @var int Order ID (if applicable) */
public $id_order;
/** @var int Customer ID (if applicable) */
public $id_customer;
/** @var int Employee ID (if applicable) */
public $id_employee;
/** @var bool Message is not displayed to the customer */
public $private;
/** @var string Object creation date */
public $date_add;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'message',
'primary' => 'id_message',
'fields' => [
'message' => ['type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'required' => true, 'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4],
'id_cart' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_order' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_customer' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_employee' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'private' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
],
];
protected $webserviceParameters = [
'fields' => [
'id_cart' => [
'xlink_resource' => 'carts',
],
'id_order' => [
'xlink_resource' => 'orders',
],
'id_customer' => [
'xlink_resource' => 'customers',
],
'id_employee' => [
'xlink_resource' => 'employees',
],
],
];
/**
* Return the last message from cart.
*
* @param int $idCart Cart ID
*
* @return array Message
*/
public static function getMessageByCartId($idCart)
{
return Db::getInstance()->getRow(
'
SELECT *
FROM `' . _DB_PREFIX_ . 'message`
WHERE `id_cart` = ' . (int) $idCart
);
}
/**
* Return messages from Order ID.
*
* @param int $idOrder Order ID
* @param bool $private return WITH private messages
*
* @return array Messages
*/
public static function getMessagesByOrderId($idOrder, bool $private = false, ?Context $context = null)
{
if (!$context) {
$context = Context::getContext();
}
return Db::getInstance()->executeS('
SELECT m.*, c.`firstname` AS cfirstname, c.`lastname` AS clastname, e.`firstname` AS efirstname, e.`lastname` AS elastname,
(COUNT(mr.id_message) = 0 AND m.id_customer != 0) AS is_new_for_me
FROM `' . _DB_PREFIX_ . 'message` m
LEFT JOIN `' . _DB_PREFIX_ . 'customer` c ON m.`id_customer` = c.`id_customer`
LEFT JOIN `' . _DB_PREFIX_ . 'message_readed` mr
ON mr.`id_message` = m.`id_message`
AND mr.`id_employee` = ' . (isset($context->employee) ? (int) $context->employee->id : '\'\'') . '
LEFT OUTER JOIN `' . _DB_PREFIX_ . 'employee` e ON e.`id_employee` = m.`id_employee`
WHERE id_order = ' . (int) $idOrder . '
' . (!$private ? ' AND m.`private` = 0' : '') . '
GROUP BY m.id_message
ORDER BY m.date_add DESC
');
}
/**
* Return messages from Cart ID.
*
* @param int $idCart Cart ID
* @param bool $private return WITH private messages
* @param Context|null $context
*
* @return array Messages
*/
public static function getMessagesByCartId($idCart, bool $private = false, ?Context $context = null)
{
if (!$context) {
$context = Context::getContext();
}
return Db::getInstance()->executeS('
SELECT m.*, c.`firstname` AS cfirstname, c.`lastname` AS clastname, e.`firstname` AS efirstname, e.`lastname` AS elastname,
(COUNT(mr.id_message) = 0 AND m.id_customer != 0) AS is_new_for_me
FROM `' . _DB_PREFIX_ . 'message` m
LEFT JOIN `' . _DB_PREFIX_ . 'customer` c ON m.`id_customer` = c.`id_customer`
LEFT JOIN `' . _DB_PREFIX_ . 'message_readed` mr ON (mr.id_message = m.id_message AND mr.id_employee = ' . (int) $context->employee->id . ')
LEFT OUTER JOIN `' . _DB_PREFIX_ . 'employee` e ON e.`id_employee` = m.`id_employee`
WHERE id_cart = ' . (int) $idCart . '
' . (!$private ? ' AND m.`private` = 0' : '') . '
GROUP BY m.id_message
ORDER BY m.date_add DESC
');
}
/**
* Registered a message 'readed'.
*
* @param int $idMessage Message ID
* @param int $idEmployee Employee ID
*
* @return bool
*/
public static function markAsReaded($idMessage, $idEmployee)
{
if (!Validate::isUnsignedId($idMessage)) {
throw new PrestaShopException('Message ID is invalid.');
}
if (!Validate::isUnsignedId($idEmployee)) {
throw new PrestaShopException('Employee ID is invalid.');
}
$result = Db::getInstance()->execute('
INSERT INTO ' . _DB_PREFIX_ . 'message_readed (id_message , id_employee , date_add) VALUES
(' . (int) $idMessage . ', ' . (int) $idEmployee . ', NOW());
');
return $result;
}
}

512
classes/Meta.php Normal file
View File

@@ -0,0 +1,512 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Adapter\Presenter\Object\ObjectPresenter;
/**
* Class MetaCore.
*/
class MetaCore extends ObjectModel
{
public $page;
public $configurable = 1;
public $title;
public $description;
public $url_rewrite;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'meta',
'primary' => 'id_meta',
'multilang' => true,
'multilang_shop' => true,
'fields' => [
'page' => ['type' => self::TYPE_STRING, 'validate' => 'isFileName', 'required' => true, 'size' => 64],
'configurable' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
/* Lang fields */
'title' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 128],
'description' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
'url_rewrite' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isLinkRewrite', 'size' => 255],
],
];
/**
* Get pages.
*
* @param bool $excludeFilled
* @param bool|string $addPage
*
* @return array
*/
public static function getPages($excludeFilled = false, $addPage = false)
{
$selectedPages = [];
if (!$files = Tools::scandir(_PS_CORE_DIR_ . DIRECTORY_SEPARATOR . 'controllers' . DIRECTORY_SEPARATOR . 'front' . DIRECTORY_SEPARATOR, 'php', '', true)) {
throw new PrestaShopException(Context::getContext()->getTranslator()->trans('Cannot scan root directory', [], 'Admin.Notifications.Error'));
}
$overrideDir = _PS_CORE_DIR_ . DIRECTORY_SEPARATOR . 'override' . DIRECTORY_SEPARATOR . 'controllers' . DIRECTORY_SEPARATOR . 'front' . DIRECTORY_SEPARATOR;
if (!is_dir($overrideDir)) {
$overrideFiles = [];
} elseif (!$overrideFiles = Tools::scandir($overrideDir, 'php', '', true)) {
throw new PrestaShopException(Context::getContext()->getTranslator()->trans('Cannot scan "override" directory', [], 'Admin.Notifications.Error'));
}
$files = array_values(array_unique(array_merge($files, $overrideFiles)));
// Exclude pages forbidden
$exludePages = [
'category',
'changecurrency',
'cms',
'footer',
'header',
'pagination',
'product',
'product-sort',
'statistics',
];
foreach ($files as $file) {
if ($file != 'index.php' && !in_array(strtolower(str_replace('Controller.php', '', $file)), $exludePages)) {
$className = str_replace('.php', '', $file);
$reflection = class_exists($className) ? new ReflectionClass(str_replace('.php', '', $file)) : false;
$properties = $reflection ? $reflection->getDefaultProperties() : [];
if (isset($properties['php_self'])) {
$selectedPages[$properties['php_self']] = $properties['php_self'];
} elseif (preg_match('/^[a-z0-9_.-]*\.php$/i', $file)) {
$selectedPages[strtolower(str_replace('Controller.php', '', $file))] = strtolower(str_replace('Controller.php', '', $file));
} elseif (preg_match('/^([a-z0-9_.-]*\/)?[a-z0-9_.-]*\.php$/i', $file)) {
$selectedPages[strtolower(Context::getContext()->getTranslator()->trans('File %2$s (in directory %1$s)', [dirname($file), str_replace('Controller.php', '', basename($file))], 'Admin.Notifications.Error'))] = strtolower(str_replace('Controller.php', '', basename($file)));
}
}
}
// Add modules controllers to list (this function is cool !)
foreach (glob(_PS_MODULE_DIR_ . '*/controllers/front/*.php') as $file) {
$filename = Tools::strtolower(basename($file, '.php'));
if ($filename == 'index') {
continue;
}
$module = Tools::strtolower(basename(dirname(dirname(dirname($file)))));
$selectedPages[$module . ' - ' . $filename] = 'module-' . $module . '-' . $filename;
}
// Exclude page already filled
if ($excludeFilled) {
$metas = Meta::getMetas();
foreach ($metas as $meta) {
if (in_array($meta['page'], $selectedPages)) {
unset($selectedPages[array_search($meta['page'], $selectedPages)]);
}
}
}
// Add selected page
if ($addPage) {
$name = $addPage;
if (preg_match('#module-([a-z0-9_-]+)-([a-z0-9]+)$#i', $addPage, $m)) {
$addPage = $m[1] . ' - ' . $m[2];
}
$selectedPages[$addPage] = $name;
asort($selectedPages);
}
return $selectedPages;
}
/**
* Get all Metas.
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public static function getMetas()
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT * FROM ' . _DB_PREFIX_ . 'meta ORDER BY page ASC');
}
/**
* Get all metas, but filter by Language.
*
* @param int $idLang Language ID
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public static function getMetasByIdLang($idLang)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT *
FROM `' . _DB_PREFIX_ . 'meta` m
LEFT JOIN `' . _DB_PREFIX_ . 'meta_lang` ml ON m.`id_meta` = ml.`id_meta`
WHERE ml.`id_lang` = ' . (int) $idLang
. Shop::addSqlRestrictionOnLang('ml') .
'ORDER BY page ASC');
}
/**
* Get metas by page.
*
* @param string $page
* @param int $idLang Language ID
*
* @return array|bool|object|null
*/
public static function getMetaByPage($page, $idLang)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
SELECT *
FROM ' . _DB_PREFIX_ . 'meta m
LEFT JOIN ' . _DB_PREFIX_ . 'meta_lang ml ON m.id_meta = ml.id_meta
WHERE (
m.page = "' . pSQL($page) . '"
OR m.page = "' . pSQL(str_replace('-', '', strtolower($page))) . '"
)
AND ml.id_lang = ' . (int) $idLang . '
' . Shop::addSqlRestrictionOnLang('ml'));
}
/**
* Get all metas.
*
* @param int $idLang
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public static function getAllMeta($idLang)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT *
FROM ' . _DB_PREFIX_ . 'meta m
LEFT JOIN ' . _DB_PREFIX_ . 'meta_lang ml ON m.id_meta = ml.id_meta
AND ml.id_lang = ' . (int) $idLang . '
' . Shop::addSqlRestrictionOnLang('ml'));
}
/**
* Updates the current Meta in the database.
*
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Indicates whether the Meta has been successfully updated
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function update($nullValues = false)
{
if (!parent::update($nullValues)) {
return false;
}
return Tools::generateHtaccess();
}
/**
* Deletes current Meta from the database.
*
* @return bool `true` if delete was successful
*
* @throws PrestaShopException
*/
public function delete()
{
if (!parent::delete()) {
return false;
}
return Tools::generateHtaccess();
}
/**
* Delete selection.
*
* @param array $selection
*
* @return bool
*/
public function deleteSelection(array $selection)
{
$result = true;
foreach ($selection as $id) {
$this->id = (int) $id;
$result = $result && $this->delete();
}
return $result && Tools::generateHtaccess();
}
/**
* Get equivalent URL rewrite.
*
* @param int $newIdLang
* @param int $idLang
* @param string $urlRewrite
*
* @return false|string|null
*/
public static function getEquivalentUrlRewrite($newIdLang, $idLang, $urlRewrite)
{
return Db::getInstance()->getValue('
SELECT url_rewrite
FROM `' . _DB_PREFIX_ . 'meta_lang`
WHERE id_meta = (
SELECT id_meta
FROM `' . _DB_PREFIX_ . 'meta_lang`
WHERE url_rewrite = \'' . pSQL($urlRewrite) . '\' AND id_lang = ' . (int) $idLang . '
AND id_shop = ' . Context::getContext()->shop->id . '
)
AND id_lang = ' . (int) $newIdLang . '
AND id_shop = ' . Context::getContext()->shop->id);
}
/**
* Get meta tags.
*/
public static function getMetaTags($idLang, $pageName, $title = '')
{
if (Configuration::get('PS_SHOP_ENABLE') || Tools::isAllowedToBypassMaintenance()) {
if ($pageName == 'product' && ($idProduct = Tools::getValue('id_product'))) {
return Meta::getProductMetas($idProduct, $idLang, $pageName);
} elseif ($pageName == 'category' && ($idCategory = Tools::getValue('id_category'))) {
return Meta::getCategoryMetas($idCategory, $idLang, $pageName, $title);
} elseif ($pageName == 'manufacturer' && ($idManufacturer = Tools::getValue('id_manufacturer'))) {
return Meta::getManufacturerMetas($idManufacturer, $idLang, $pageName);
} elseif ($pageName == 'supplier' && ($idSupplier = Tools::getValue('id_supplier'))) {
return Meta::getSupplierMetas($idSupplier, $idLang, $pageName);
} elseif ($pageName == 'cms' && ($idCms = Tools::getValue('id_cms'))) {
return Meta::getCmsMetas($idCms, $idLang, $pageName);
} elseif ($pageName == 'cms' && ($idCmsCategory = Tools::getValue('id_cms_category'))) {
return Meta::getCmsCategoryMetas($idCmsCategory, $idLang, $pageName);
}
}
return Meta::getHomeMetas($idLang, $pageName);
}
/**
* Get meta tags for a given page.
*
* @param int $idLang Language ID
* @param string $pageName Page name
*
* @return array Meta tags
*/
public static function getHomeMetas($idLang, $pageName)
{
$metas = Meta::getMetaByPage($pageName, $idLang);
$ret['meta_title'] = (isset($metas['title']) && $metas['title']) ? $metas['title'] : Configuration::get('PS_SHOP_NAME');
$ret['meta_description'] = (isset($metas['description']) && $metas['description']) ? $metas['description'] : '';
$ret = Meta::completeMetaTags($ret, $ret['meta_title']);
return $ret;
}
/**
* Get product meta tags.
*
* @param int $idProduct
* @param int $idLang
* @param string $pageName
*
* @return array
*/
public static function getProductMetas($idProduct, $idLang, $pageName)
{
$product = new Product($idProduct, false, $idLang);
if (Validate::isLoadedObject($product) && $product->active) {
$row = Meta::getPresentedObject($product);
if (empty($row['meta_description'])) {
$row['meta_description'] = strip_tags($row['description_short']);
}
return Meta::completeMetaTags($row, $row['name']);
}
return Meta::getHomeMetas($idLang, $pageName);
}
/**
* Get category meta tags.
*
* @param int $idCategory
* @param int $idLang
* @param string $pageName
*
* @return array
*/
public static function getCategoryMetas($idCategory, $idLang, $pageName, $title = '')
{
$category = new Category($idCategory, $idLang);
$cacheId = 'Meta::getCategoryMetas' . (int) $idCategory . '-' . (int) $idLang;
if (!Cache::isStored($cacheId)) {
if (Validate::isLoadedObject($category)) {
$row = Meta::getPresentedObject($category);
if (empty($row['meta_description'])) {
$row['meta_description'] = strip_tags($row['description']);
}
if (is_string($title) && $title !== '') {
$row['meta_title'] = $title;
} else {
$row['meta_title'] = $row['meta_title'] ?: $row['name'];
}
$result = Meta::completeMetaTags($row, $row['name']);
} else {
$result = Meta::getHomeMetas($idLang, $pageName);
}
Cache::store($cacheId, $result);
return $result;
}
return Cache::retrieve($cacheId);
}
/**
* Get manufacturer meta tags.
*
* @param int $idManufacturer
* @param int $idLang
* @param string $pageName
*
* @return array
*/
public static function getManufacturerMetas($idManufacturer, $idLang, $pageName)
{
$manufacturer = new Manufacturer($idManufacturer, $idLang);
if (Validate::isLoadedObject($manufacturer)) {
$row = Meta::getPresentedObject($manufacturer);
if (!empty($row['meta_description'])) {
$row['meta_description'] = strip_tags($row['meta_description']);
}
$row['meta_title'] = $row['meta_title'] ?: $row['name'];
return Meta::completeMetaTags($row, $row['meta_title']);
}
return Meta::getHomeMetas($idLang, $pageName);
}
/**
* Get supplier meta tags.
*
* @param int $idSupplier
* @param int $idLang
* @param string $pageName
*
* @return array
*/
public static function getSupplierMetas($idSupplier, $idLang, $pageName)
{
$supplier = new Supplier($idSupplier, $idLang);
if (Validate::isLoadedObject($supplier)) {
$row = Meta::getPresentedObject($supplier);
if (!empty($row['meta_description'])) {
$row['meta_description'] = strip_tags($row['meta_description']);
}
return Meta::completeMetaTags($row, $row['name']);
}
return Meta::getHomeMetas($idLang, $pageName);
}
/**
* Get CMS meta tags.
*
* @param int $idCms
* @param int $idLang
* @param string $pageName
*
* @return array
*/
public static function getCmsMetas($idCms, $idLang, $pageName)
{
$cms = new CMS($idCms, $idLang);
if (Validate::isLoadedObject($cms)) {
$row = Meta::getPresentedObject($cms);
$row['meta_title'] = !empty($row['head_seo_title']) ? $row['head_seo_title'] : $row['meta_title'];
return Meta::completeMetaTags($row, $row['meta_title']);
}
return Meta::getHomeMetas($idLang, $pageName);
}
/**
* Get CMS category meta tags.
*
* @param int $idCmsCategory
* @param int $idLang
* @param string $pageName
*
* @return array
*/
public static function getCmsCategoryMetas($idCmsCategory, $idLang, $pageName)
{
$cmsCategory = new CMSCategory($idCmsCategory, $idLang);
if (Validate::isLoadedObject($cmsCategory)) {
$row = Meta::getPresentedObject($cmsCategory);
$row['meta_title'] = empty($row['meta_title']) ? $row['name'] : $row['meta_title'];
return Meta::completeMetaTags($row, $row['meta_title']);
}
return Meta::getHomeMetas($idLang, $pageName);
}
public static function completeMetaTags($metaTags, $defaultValue, ?Context $context = null)
{
if (!$context) {
$context = Context::getContext();
}
if (empty($metaTags['meta_title'])) {
$metaTags['meta_title'] = $defaultValue;
}
if (!empty($context->controller) && method_exists($context->controller, 'getTemplateVarPagination')) {
$metaTags['meta_title'] = Meta::paginateTitle($metaTags['meta_title']);
}
return $metaTags;
}
/**
* Add page number to title
*
* @param string $title
*
* @return string
*/
public static function paginateTitle(string $title): string
{
$page_num = (int) Tools::getValue('page');
if ($page_num > 1) {
$title .= ' (' . $page_num . ')';
}
return $title;
}
/**
* Get presented version of an object.
*
* @param ObjectModel $object
*
* @return array
*/
protected static function getPresentedObject($object)
{
$objectPresenter = new ObjectPresenter();
return $objectPresenter->present($object);
}
}

216
classes/Notification.php Normal file
View File

@@ -0,0 +1,216 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class NotificationCore.
*/
class NotificationCore
{
public $types;
/**
* NotificationCore constructor.
*/
public function __construct()
{
$this->types = ['order', 'customer_message', 'customer'];
}
/**
* getLastElements return all the notifications (new order, new customer registration, and new customer message)
* Get all the notifications.
*
* @return array containing the notifications
*/
public function getLastElements()
{
$notifications = [];
$employeeInfos = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
SELECT id_last_order, id_last_customer_message, id_last_customer
FROM `' . _DB_PREFIX_ . 'employee`
WHERE `id_employee` = ' . (int) Context::getContext()->employee->id);
foreach ($this->types as $type) {
$notifications[$type] = Notification::getLastElementsIdsByType($type, $employeeInfos['id_last_' . $type]);
}
return $notifications;
}
/**
* getActiveLastElements returns all allowed notifications in the backoffice
* Get allowed notifications.
*
* @return array containing the notifications
*/
public function getActiveLastElements(): array
{
$types = array_flip($this->types);
if (!(bool) Configuration::get('PS_SHOW_NEW_ORDERS')) {
unset($types['order']);
}
if (!(bool) Configuration::get('PS_SHOW_NEW_CUSTOMERS')) {
unset($types['customer']);
}
if (!(bool) Configuration::get('PS_SHOW_NEW_MESSAGES')) {
unset($types['customer_message']);
}
if (0 == count($types)) {
return [];
}
$notifications = [];
$employeeInfos = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
SELECT id_last_order, id_last_customer_message, id_last_customer
FROM `' . _DB_PREFIX_ . 'employee`
WHERE `id_employee` = ' . (int) Context::getContext()->employee->id);
$types = array_flip($types);
foreach ($types as $type) {
$notifications[$type] = Notification::getLastElementsIdsByType($type, $employeeInfos['id_last_' . $type]);
}
return $notifications;
}
/**
* getLastElementsIdsByType return all the element ids to show (order, customer registration, and customer message)
* Get all the element ids.
*
* @param string $type contains the field name of the Employee table
* @param int $idLastElement contains the id of the last seen element
*
* @return array containing the notifications
*/
public static function getLastElementsIdsByType($type, $idLastElement)
{
global $cookie;
switch ($type) {
case 'order':
$sql = '
SELECT SQL_CALC_FOUND_ROWS o.`id_order`, o.`id_customer`, o.`total_paid`, o.`id_currency`, o.`date_upd`, c.`firstname`, c.`lastname`, ca.`name`, co.`iso_code`
FROM `' . _DB_PREFIX_ . 'orders` as o
LEFT JOIN `' . _DB_PREFIX_ . 'customer` as c ON (c.`id_customer` = o.`id_customer`)
LEFT JOIN `' . _DB_PREFIX_ . 'carrier` as ca ON (ca.`id_carrier` = o.`id_carrier`)
LEFT JOIN `' . _DB_PREFIX_ . 'address` as a ON (a.`id_address` = o.`id_address_delivery`)
LEFT JOIN `' . _DB_PREFIX_ . 'country` as co ON (co.`id_country` = a.`id_country`)
WHERE `id_order` > ' . (int) $idLastElement .
Shop::addSqlRestriction(false, 'o') . '
ORDER BY `id_order` DESC
LIMIT 5';
break;
case 'customer_message':
$sql = '
SELECT SQL_CALC_FOUND_ROWS c.`id_customer_message`, ct.`id_customer`, ct.`id_customer_thread`, ct.`email`, ct.`status`, c.`date_add`, cu.`firstname`, cu.`lastname`
FROM `' . _DB_PREFIX_ . 'customer_message` as c
LEFT JOIN `' . _DB_PREFIX_ . 'customer_thread` as ct ON (c.`id_customer_thread` = ct.`id_customer_thread`)
LEFT JOIN `' . _DB_PREFIX_ . 'customer` as cu ON (cu.`id_customer` = ct.`id_customer`)
WHERE c.`id_customer_message` > ' . (int) $idLastElement . '
AND c.`id_employee` = 0
AND ct.id_shop IN (' . implode(', ', Shop::getContextListShopID()) . ')
ORDER BY c.`id_customer_message` DESC
LIMIT 5';
break;
default:
$sql = '
SELECT SQL_CALC_FOUND_ROWS t.`id_' . bqSQL($type) . '`, t.*
FROM `' . _DB_PREFIX_ . bqSQL($type) . '` t
WHERE t.`deleted` = 0 AND t.`id_' . bqSQL($type) . '` > ' . (int) $idLastElement .
Shop::addSqlRestriction(false, 't') . '
ORDER BY t.`id_' . bqSQL($type) . '` DESC
LIMIT 5';
break;
}
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql, true, false);
$total = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('SELECT FOUND_ROWS()', false);
$json = ['total' => $total, 'results' => []];
foreach ($result as $value) {
$customerName = '';
if (isset($value['firstname'], $value['lastname'])) {
$customerName = Tools::safeOutput($value['firstname'] . ' ' . $value['lastname']);
} elseif (isset($value['email'])) {
$customerName = Tools::safeOutput($value['email']);
}
$json['results'][] = [
'id_order' => ((!empty($value['id_order'])) ? (int) $value['id_order'] : 0),
'id_customer' => ((!empty($value['id_customer'])) ? (int) $value['id_customer'] : 0),
'id_customer_message' => ((!empty($value['id_customer_message'])) ? (int) $value['id_customer_message'] : 0),
'id_customer_thread' => ((!empty($value['id_customer_thread'])) ? (int) $value['id_customer_thread'] : 0),
'total_paid' => ((!empty($value['total_paid'])) ? Tools::getContextLocale(Context::getContext())->formatPrice((float) $value['total_paid'], Currency::getIsoCodeById((int) $value['id_currency'])) : 0),
'carrier' => ((!empty($value['name'])) ? Tools::safeOutput($value['name']) : ''),
'iso_code' => ((!empty($value['iso_code'])) ? Tools::safeOutput($value['iso_code']) : ''),
'company' => ((!empty($value['company'])) ? Tools::safeOutput($value['company']) : ''),
'status' => ((!empty($value['status'])) ? Tools::safeOutput($value['status']) : ''),
'customer_name' => $customerName,
'date_add' => isset($value['date_add']) ? Tools::displayDate($value['date_add']) : 0,
'order_view_url' => !empty($value['id_order'])
? Context::getContext()->link->getAdminLink(
'AdminOrders',
true,
[
'orderId' => $value['id_order'],
'vieworder' => true,
]
)
: '',
'customer_view_url' => Context::getContext()->link->getAdminLink(
'AdminCustomers',
true,
[
'customerId' => $value['id_customer'],
'viewcustomer' => true,
]
),
'customer_thread_view_url' => !empty($value['id_customer_thread'])
? Context::getContext()->link->getAdminLink(
'AdminCustomerThreads',
true,
[
'customerThreadId' => $value['id_customer_thread'],
'viewcustomer_thread' => true,
]
)
: '',
];
}
return $json;
}
/**
* updateEmployeeLastElement return 0 if the field doesn't exists in Employee table.
* Updates the last seen element by the employee.
*
* @param string $type contains the field name of the Employee table
*
* @return bool if type exists or not
*/
public function updateEmployeeLastElement($type)
{
if (in_array($type, $this->types)) {
// We update the last item viewed
return Db::getInstance()->execute('
UPDATE `' . _DB_PREFIX_ . 'employee`
SET `id_last_' . bqSQL($type) . '` = (
SELECT IFNULL(MAX(`id_' . bqSQL($type) . '`), 0)
FROM `' . _DB_PREFIX_ . (($type == 'order') ? bqSQL($type) . 's' : bqSQL($type)) . '`
)
WHERE `id_employee` = ' . (int) Context::getContext()->employee->id);
}
return false;
}
}

2129
classes/ObjectModel.php Normal file

File diff suppressed because it is too large Load Diff

604
classes/Pack.php Normal file
View File

@@ -0,0 +1,604 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Core\Domain\Product\Pack\ValueObject\PackStockType;
use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductType;
class PackCore extends Product
{
/**
* Only decrement pack quantity.
*
* @var int
*/
public const STOCK_TYPE_PACK_ONLY = PackStockType::STOCK_TYPE_PACK_ONLY;
/**
* Only decrement pack products quantities.
*
* @var int
*/
public const STOCK_TYPE_PRODUCTS_ONLY = PackStockType::STOCK_TYPE_PRODUCTS_ONLY;
/**
* Decrement pack quantity and pack products quantities.
*
* @var int
*/
public const STOCK_TYPE_PACK_BOTH = PackStockType::STOCK_TYPE_BOTH;
/**
* Use pack quantity default setting.
*
* @var int
*/
public const STOCK_TYPE_DEFAULT = PackStockType::STOCK_TYPE_DEFAULT;
protected static $cachePackItems = [];
protected static $cacheIsPack = [];
protected static $cacheIsPacked = [];
public static function resetStaticCache()
{
self::$cachePackItems = [];
self::$cacheIsPack = [];
self::$cacheIsPacked = [];
}
/**
* Is product a pack?
*
* @param int $id_product
*
* @return bool
*/
public static function isPack($id_product)
{
if (!Pack::isFeatureActive()) {
return false;
}
if (!$id_product) {
return false;
}
if (!array_key_exists($id_product, self::$cacheIsPack)) {
// This is not very efficient, isn't an entry in pack table a proof that it's a pack?
// Moreover, we already have cache_is_pack column, product_type is just a duplicate.
$result = Db::getInstance()->getValue('SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'pack` WHERE id_product_pack = ' . (int) $id_product);
$productType = Db::getInstance()->getValue('SELECT product_type FROM `' . _DB_PREFIX_ . 'product` WHERE id_product = ' . (int) $id_product);
self::$cacheIsPack[$id_product] = ($result > 0) || $productType === ProductType::TYPE_PACK;
}
return self::$cacheIsPack[$id_product];
}
/**
* Is product in a pack?
* If $id_product_attribute specified, then will restrict search on the given combination,
* else this method will match a product if at least one of all its combination is in a pack.
*
* @param int $id_product
* @param int|bool $id_product_attribute Optional combination of the product
*
* @return bool
*/
public static function isPacked($id_product, $id_product_attribute = false)
{
if (!Pack::isFeatureActive()) {
return false;
}
if ($id_product_attribute === false) {
$cache_key = $id_product . '-0';
if (!array_key_exists($cache_key, self::$cacheIsPacked)) {
$result = Db::getInstance()->getValue('SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'pack` WHERE id_product_item = ' . (int) $id_product);
self::$cacheIsPacked[$cache_key] = ($result > 0);
}
return self::$cacheIsPacked[$cache_key];
} else {
$cache_key = $id_product . '-' . $id_product_attribute;
if (!array_key_exists($cache_key, self::$cacheIsPacked)) {
$result = Db::getInstance()->getValue('SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'pack` WHERE id_product_item = ' . ((int) $id_product) . ' AND
id_product_attribute_item = ' . ((int) $id_product_attribute));
self::$cacheIsPacked[$cache_key] = ($result > 0);
}
return self::$cacheIsPacked[$cache_key];
}
}
public static function noPackPrice($id_product)
{
$sum = 0;
$price_display_method = !self::$_taxCalculationMethod;
$items = Pack::getItems($id_product, Configuration::get('PS_LANG_DEFAULT'));
foreach ($items as $item) {
$pricePerItem = $item->getPrice($price_display_method, $item->id_pack_product_attribute ? $item->id_pack_product_attribute : null);
// Different calculation depending on rounding type
switch (Configuration::get('PS_ROUND_TYPE')) {
case Order::ROUND_TOTAL:
$sum += $pricePerItem * $item->pack_quantity;
break;
case Order::ROUND_LINE:
$sum += Tools::ps_round(
$pricePerItem * $item->pack_quantity,
Context::getContext()->getComputingPrecision()
);
break;
case Order::ROUND_ITEM:
default:
$sum += Tools::ps_round(
$pricePerItem,
Context::getContext()->getComputingPrecision()
) * $item->pack_quantity;
break;
}
}
return $sum;
}
public static function noPackWholesalePrice($id_product)
{
$sum = 0;
$items = Pack::getItems($id_product, Configuration::get('PS_LANG_DEFAULT'));
foreach ($items as $item) {
$sum += $item->wholesale_price * $item->pack_quantity;
}
return $sum;
}
public static function getItems($id_product, $id_lang)
{
if (!Pack::isFeatureActive()) {
return [];
}
if (array_key_exists($id_product, self::$cachePackItems)) {
return self::$cachePackItems[$id_product];
}
$result = Db::getInstance()->executeS('SELECT id_product_item, id_product_attribute_item, quantity FROM `' . _DB_PREFIX_ . 'pack` where id_product_pack = ' . (int) $id_product);
$array_result = [];
foreach ($result as $row) {
$p = new Product($row['id_product_item'], false, $id_lang);
$p->loadStockData();
$p->pack_quantity = $row['quantity'];
$p->id_pack_product_attribute = (isset($row['id_product_attribute_item']) && $row['id_product_attribute_item'] ? $row['id_product_attribute_item'] : 0);
if (isset($row['id_product_attribute_item']) && $row['id_product_attribute_item']) {
$sql = 'SELECT agl.`name` AS group_name, al.`name` AS attribute_name, pa.`reference` AS attribute_reference
FROM `' . _DB_PREFIX_ . 'product_attribute` pa
' . Shop::addSqlAssociation('product_attribute', 'pa') . '
LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON pac.`id_product_attribute` = pa.`id_product_attribute`
LEFT JOIN `' . _DB_PREFIX_ . 'attribute` a ON a.`id_attribute` = pac.`id_attribute`
LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group` ag ON ag.`id_attribute_group` = a.`id_attribute_group`
LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) Context::getContext()->language->id . ')
LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) Context::getContext()->language->id . ')
WHERE pa.`id_product_attribute` = ' . $row['id_product_attribute_item'] . '
GROUP BY pa.`id_product_attribute`, ag.`id_attribute_group`
ORDER BY pa.`id_product_attribute`';
$combinations = Db::getInstance()->executeS($sql);
foreach ($combinations as $k => $combination) {
$p->name .= ' ' . $combination['group_name'] . '-' . $combination['attribute_name'];
$p->reference = $combination['attribute_reference'];
}
}
$array_result[] = $p;
}
self::$cachePackItems[$id_product] = $array_result;
return self::$cachePackItems[$id_product];
}
/**
* Indicates if a pack and its associated products are available for orders in the desired quantity.
*
* @todo This method returns true even if the pack feature is not active.
* Should throw an exception instead.
* Developers should first test if product is a pack
* and then if it's in stock.
*
* @param int $idProduct
* @param int $wantedQuantity
* @param Cart|null $cart
*
* @return bool
*
* @throws PrestaShopException
*/
public static function isInStock($idProduct, $wantedQuantity = 1, ?Cart $cart = null)
{
if (!Pack::isFeatureActive()) {
return true;
}
$idProduct = (int) $idProduct;
$wantedQuantity = (int) $wantedQuantity;
$product = new Product($idProduct, false);
$packQuantity = self::getQuantity($idProduct, null, null, $cart, false);
if ($product->isAvailableWhenOutOfStock($product->out_of_stock)) {
return true;
} elseif ($wantedQuantity > $packQuantity) {
return false;
}
return true;
}
/**
* Returns the available quantity of a given pack.
*
* By default, it returns the TRUE quantity in stock. If you want to, you can pass a $cart parameter
* and the quantity in stock will be reduced by the quantity there is in the cart.
*
* @param int $idProduct Product id
* @param int|null $idProductAttribute Product attribute id (optional)
* @param bool|null $cacheIsPack (unused, you can pass null)
* @param CartCore|null $cart Pass if you want to reduce the quantity by amount in cart
* @param int|bool|null $idCustomization Product customization id (optional)
*
* @return int
*
* @throws PrestaShopException
*/
public static function getQuantity(
$idProduct,
$idProductAttribute = null,
$cacheIsPack = null,
?CartCore $cart = null,
$idCustomization = null
) {
$idProduct = (int) $idProduct;
$idProductAttribute = (int) $idProductAttribute;
if (!self::isPack($idProduct)) {
throw new PrestaShopException("Product with id $idProduct is not a pack");
}
// Initialize
$product = new Product($idProduct, false);
$packQuantity = 0;
// We get the pack stock calculation type it has set up
$packStockType = $product->pack_stock_type;
$allPackStockType = [
self::STOCK_TYPE_PACK_ONLY,
self::STOCK_TYPE_PRODUCTS_ONLY,
self::STOCK_TYPE_PACK_BOTH,
self::STOCK_TYPE_DEFAULT,
];
if (!in_array($packStockType, $allPackStockType)) {
throw new PrestaShopException('Unknown pack stock type');
}
/*
* Now, we have resolved how we will calculate the stock of this pack. It can be one of the following.
*
* STOCK_TYPE_PACK_ONLY - pack 1pcs + product A 10pcs + product B 20pcs = 1pcs
* STOCK_TYPE_PRODUCTS_ONLY - pack 1pcs + product A 10pcs + product B 20pcs = 10 pcs
* STOCK_TYPE_PACK_BOTH - pack 1pcs + product A 10pcs + product B 20pcs = 1 pcs
*/
// If no pack stock or shop default, set it from configuration
if (empty($packStockType) || $packStockType == self::STOCK_TYPE_DEFAULT) {
$packStockType = Configuration::get('PS_PACK_STOCK_TYPE');
}
// If the quantity of the pack depends only on the pack or both packs and products,
// we need to load the quantity of the pack from stock_available table.
if (in_array($packStockType, [self::STOCK_TYPE_PACK_ONLY, self::STOCK_TYPE_PACK_BOTH])) {
$packQuantity = StockAvailable::getQuantityAvailableByProduct(
$idProduct,
$idProductAttribute
);
}
// If the quantity of the pack depends on the products inside, or both pack and products,
// we need to set the pack quantity to the lowest quantity of products inside.
if (in_array($packStockType, [self::STOCK_TYPE_PACK_BOTH, self::STOCK_TYPE_PRODUCTS_ONLY])) {
$items = array_values(Pack::getItems($idProduct, Configuration::get('PS_LANG_DEFAULT')));
foreach ($items as $index => $item) {
$itemQuantity = Product::getQuantity($item->id, $item->id_pack_product_attribute ?: null, null, $cart, $idCustomization);
$nbPackAvailableForItem = (int) floor($itemQuantity / $item->pack_quantity);
// Initialize packQuantity with the first product quantity
// if pack decrement stock type is products only
// @todo This is probably not needed because $packQuantity is always initialized to zero.
if ($index === 0
&& $packStockType == self::STOCK_TYPE_PRODUCTS_ONLY
) {
$packQuantity = $nbPackAvailableForItem;
continue;
}
// If the quantity of the individual item is lower than what we currently calculated, it's our new quantity.
if ($nbPackAvailableForItem < $packQuantity) {
$packQuantity = $nbPackAvailableForItem;
}
}
} elseif (!empty($cart)) {
$cartProduct = $cart->getProductQuantity($idProduct, $idProductAttribute, $idCustomization);
if (!empty($cartProduct['deep_quantity'])) {
$packQuantity -= $cartProduct['deep_quantity'];
}
}
return $packQuantity;
}
public static function getItemTable($id_product, $id_lang, $full = false)
{
if (!Pack::isFeatureActive()) {
return [];
}
$context = Context::getContext();
$sql = 'SELECT p.*, product_shop.*, pl.*, image_shop.`id_image` id_image, il.`legend`, cl.`name` AS category_default, a.quantity AS pack_quantity, product_shop.`id_category_default`, a.id_product_pack, a.id_product_attribute_item
FROM `' . _DB_PREFIX_ . 'pack` a
LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON p.id_product = a.id_product_item
LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl
ON p.id_product = pl.id_product
AND pl.`id_lang` = ' . (int) $id_lang . Shop::addSqlRestrictionOnLang('pl') . '
LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
ON (image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop=' . (int) $context->shop->id . ')
LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $id_lang . ')
' . Shop::addSqlAssociation('product', 'p') . '
LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl
ON product_shop.`id_category_default` = cl.`id_category`
AND cl.`id_lang` = ' . (int) $id_lang . Shop::addSqlRestrictionOnLang('cl') . '
WHERE product_shop.`id_shop` = ' . (int) $context->shop->id . '
AND a.`id_product_pack` = ' . (int) $id_product . '
GROUP BY a.`id_product_item`, a.`id_product_attribute_item`';
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
/** @var array{id_product: int, id_product_attribute_item: int|null, name: string} $line */
foreach ($result as &$line) {
if (Combination::isFeatureActive() && isset($line['id_product_attribute_item']) && $line['id_product_attribute_item']) {
$line['cache_default_attribute'] = $line['id_product_attribute'] = $line['id_product_attribute_item'];
$sql = 'SELECT pa.`reference` AS attribute_reference, agl.`name` AS group_name, al.`name` AS attribute_name, pai.`id_image` AS id_product_attribute_image
FROM `' . _DB_PREFIX_ . 'product_attribute` pa
' . Shop::addSqlAssociation('product_attribute', 'pa') . '
LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON pac.`id_product_attribute` = ' . $line['id_product_attribute_item'] . '
LEFT JOIN `' . _DB_PREFIX_ . 'attribute` a ON a.`id_attribute` = pac.`id_attribute`
LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group` ag ON ag.`id_attribute_group` = a.`id_attribute_group`
LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) Context::getContext()->language->id . ')
LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) Context::getContext()->language->id . ')
LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_image` pai ON (' . $line['id_product_attribute_item'] . ' = pai.`id_product_attribute`)
WHERE pa.`id_product` = ' . (int) $line['id_product'] . ' AND pa.`id_product_attribute` = ' . $line['id_product_attribute_item'] . '
GROUP BY pa.`id_product_attribute`, ag.`id_attribute_group`
ORDER BY pa.`id_product_attribute`';
$attr_name = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
if (isset($attr_name[0]['id_product_attribute_image']) && $attr_name[0]['id_product_attribute_image']) {
$line['id_image'] = $attr_name[0]['id_product_attribute_image'];
}
$line['reference'] = $attr_name[0]['attribute_reference'] ?? '';
$line['name'] .= "\n";
foreach ($attr_name as $value) {
$line['name'] .= ' ' . $value['group_name'] . '-' . $value['attribute_name'];
}
}
$line = Product::getTaxesInformations($line);
}
foreach ($result as $k => $v) {
if (!Pack::isPack($v['id_product'])) {
$result[$k]['id_product_attribute'] = (int) $v['id_product_attribute_item'];
}
}
return $result;
}
public static function getPacksTable($id_product, $id_lang, $full = false, $limit = null)
{
if (!Pack::isFeatureActive()) {
return [];
}
$packs = Db::getInstance()->getValue('
SELECT GROUP_CONCAT(a.`id_product_pack`)
FROM `' . _DB_PREFIX_ . 'pack` a
WHERE a.`id_product_item` = ' . (int) $id_product);
if (!(int) $packs) {
return [];
}
$context = Context::getContext();
$sql = '
SELECT p.*, product_shop.*, pl.*, image_shop.`id_image` id_image, il.`legend`, IFNULL(product_attribute_shop.id_product_attribute, 0) id_product_attribute
FROM `' . _DB_PREFIX_ . 'product` p
NATURAL LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl
' . Shop::addSqlAssociation('product', 'p') . '
LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` product_attribute_shop
ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shop.id_shop=' . (int) $context->shop->id . ')
LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
ON (image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop=' . (int) $context->shop->id . ')
LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $id_lang . ')
WHERE pl.`id_lang` = ' . (int) $id_lang . '
' . Shop::addSqlRestrictionOnLang('pl') . '
AND p.`id_product` IN (' . $packs . ')
GROUP BY p.id_product';
if ($limit) {
$sql .= ' LIMIT ' . (int) $limit;
}
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
if (!$full) {
return $result;
}
$array_result = [];
foreach ($result as $row) {
if (!Pack::isPacked($row['id_product'])) {
$array_result[] = $row;
}
}
return $array_result;
}
public static function deleteItems($id_product, $refreshCache = true)
{
$result = true;
if ($refreshCache) {
$result = Db::getInstance()->update('product', ['cache_is_pack' => 0], 'id_product = ' . (int) $id_product);
}
return $result
&& Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'pack` WHERE `id_product_pack` = ' . (int) $id_product)
&& Configuration::updateGlobalValue('PS_PACK_FEATURE_ACTIVE', Pack::isCurrentlyUsed());
}
/**
* Add an item to the pack.
*
* @param int $id_product
* @param int $id_item
* @param int $qty
* @param int $id_attribute_item
*
* @return bool true if everything was fine
*
* @throws PrestaShopDatabaseException
*/
public static function addItem($id_product, $id_item, $qty, $id_attribute_item = 0)
{
$id_attribute_item = (int) $id_attribute_item ? (int) $id_attribute_item : Product::getDefaultAttribute((int) $id_item);
return Db::getInstance()->update('product', ['cache_is_pack' => 1, 'product_type' => ProductType::TYPE_PACK], 'id_product = ' . (int) $id_product)
&& Db::getInstance()->insert('pack', [
'id_product_pack' => (int) $id_product,
'id_product_item' => (int) $id_item,
'id_product_attribute_item' => (int) $id_attribute_item,
'quantity' => (int) $qty,
])
&& Configuration::updateGlobalValue('PS_PACK_FEATURE_ACTIVE', '1');
}
public static function duplicate($id_product_old, $id_product_new)
{
Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'pack` (`id_product_pack`, `id_product_item`, `id_product_attribute_item`, `quantity`)
(SELECT ' . (int) $id_product_new . ', `id_product_item`, `id_product_attribute_item`, `quantity` FROM `' . _DB_PREFIX_ . 'pack` WHERE `id_product_pack` = ' . (int) $id_product_old . ')');
// If return query result, a non-pack product will return false
return true;
}
/**
* This method is allow to know if a feature is used or active.
*
* @return bool
*/
public static function isFeatureActive()
{
return Configuration::get('PS_PACK_FEATURE_ACTIVE');
}
/**
* This method is allow to know if a Pack entity is currently used.
*
* @param string|null $table Name of table linked to entity
* @param bool $has_active_column True if the table has an active column
*
* @return bool
*/
public static function isCurrentlyUsed($table = null, $has_active_column = false)
{
// We dont't use the parent method because the identifier isn't id_pack
return (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
SELECT `id_product_pack`
FROM `' . _DB_PREFIX_ . 'pack`
');
}
/**
* For a given pack, tells if it has at least one product using the advanced stock management.
*
* @param int $id_product id_pack
*
* @return bool
*
* @deprecated Since 9.0 and will be removed in 10.0
*/
public static function usesAdvancedStockManagement($id_product)
{
@trigger_error(sprintf(
'%s is deprecated since 9.0 and will be removed in 10.0.',
__METHOD__
), E_USER_DEPRECATED);
return false;
}
/**
* For a given pack, tells if all products using the advanced stock management.
*
* @param int $id_product id_pack
*
* @return bool
*
* @deprecated Since 9.0 and will be removed in 10.0
*/
public static function allUsesAdvancedStockManagement($id_product)
{
@trigger_error(sprintf(
'%s is deprecated since 9.0 and will be removed in 10.0.',
__METHOD__
), E_USER_DEPRECATED);
return false;
}
/**
* Returns Packs that contains the given product in the right combination.
*
* @param int $id_item Product item id that could be contained in a|many pack(s)
* @param int $id_attribute_item The combination of the product
* @param int $id_lang
*
* @return array[Product] Packs that contains the given product
*/
public static function getPacksContainingItem($id_item, $id_attribute_item, $id_lang)
{
if (!Pack::isFeatureActive() || !$id_item) {
return [];
}
$query = 'SELECT `id_product_pack`, `quantity` FROM `' . _DB_PREFIX_ . 'pack`
WHERE `id_product_item` = ' . ((int) $id_item);
if (Combination::isFeatureActive()) {
$query .= ' AND `id_product_attribute_item` = ' . ((int) $id_attribute_item);
}
$result = Db::getInstance()->executeS($query);
$array_result = [];
foreach ($result as $row) {
$p = new Product($row['id_product_pack'], true, $id_lang);
$p->loadStockData();
$p->pack_item_quantity = $row['quantity']; // Specific need from StockAvailable::updateQuantity()
$array_result[] = $p;
}
return $array_result;
}
}

127
classes/Page.php Normal file
View File

@@ -0,0 +1,127 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class PageCore.
*/
class PageCore extends ObjectModel
{
public $id_page_type;
public $id_object;
public $name;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'page',
'primary' => 'id_page',
'fields' => [
'id_page_type' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_object' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
],
];
/**
* @return int Current page ID
*/
public static function getCurrentId()
{
$controller = Dispatcher::getInstance()->getController();
$pageTypeId = Page::getPageTypeByName($controller);
/**
* Some pages must be distinguished in order to record exactly what is being seen
*
* @todo dispatcher module
*/
$specialArray = [
'product' => 'id_product',
'category' => 'id_category',
'order' => 'step',
'manufacturer' => 'id_manufacturer',
];
$where = '';
$insertData = [
'id_page_type' => $pageTypeId,
];
if (array_key_exists($controller, $specialArray)) {
$objectId = Tools::getValue($specialArray[$controller], null);
$where = ' AND `id_object` = ' . (int) $objectId;
$insertData['id_object'] = (int) $objectId;
}
$sql = 'SELECT `id_page`
FROM `' . _DB_PREFIX_ . 'page`
WHERE `id_page_type` = ' . (int) $pageTypeId . $where;
$result = Db::getInstance()->getRow($sql);
if (!empty($result['id_page'])) {
return $result['id_page'];
}
Db::getInstance()->insert('page', $insertData, true);
return Db::getInstance()->Insert_ID();
}
/**
* Return page type ID from page name.
*
* @param string $name Page name (E.g. product.php)
*/
public static function getPageTypeByName($name)
{
if ($value = Db::getInstance()->getValue(
'
SELECT id_page_type
FROM ' . _DB_PREFIX_ . 'page_type
WHERE name = \'' . pSQL($name) . '\''
)
) {
return $value;
}
Db::getInstance()->insert('page_type', ['name' => pSQL($name)]);
return Db::getInstance()->Insert_ID();
}
/**
* Increase page viewed number by one.
*
* @param int $idPage Page ID
*/
public static function setPageViewed($idPage)
{
$idDateRange = DateRange::getCurrentRange();
$context = Context::getContext();
// Try to increment the visits counter
$sql = 'UPDATE `' . _DB_PREFIX_ . 'page_viewed`
SET `counter` = `counter` + 1
WHERE `id_date_range` = ' . (int) $idDateRange . '
AND `id_page` = ' . (int) $idPage . '
AND `id_shop` = ' . (int) $context->shop->id;
Db::getInstance()->execute($sql);
// If no one has seen the page in this date range, it is added
if (Db::getInstance()->Affected_Rows() == 0) {
Db::getInstance()->insert(
'page_viewed',
[
'id_date_range' => (int) $idDateRange,
'id_page' => (int) $idPage,
'counter' => 1,
'id_shop' => (int) $context->shop->id,
'id_shop_group' => (int) $context->shop->id_shop_group,
]
);
}
}
}

19
classes/PaymentFree.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class PaymentFree
* Simple class to allow free order.
*/
class PaymentFree extends PaymentModule
{
/** @var bool Status */
public $active = true;
/** @var string */
public $name = 'free_order';
/** @var string */
public $displayName = 'Free order';
}

1364
classes/PaymentModule.php Normal file

File diff suppressed because it is too large Load Diff

97
classes/PhpEncryption.php Normal file
View File

@@ -0,0 +1,97 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use Defuse\Crypto\Exception\EnvironmentIsBrokenException;
/**
* Class PhpEncryptionCore for openSSL 1.0.1+.
*/
class PhpEncryptionCore
{
public const ENGINE = 'PhpEncryptionEngine';
private static $engine;
/**
* PhpEncryptionCore constructor.
*
* @param string $hexString A string that only contains hexadecimal characters
* Bother upper and lower case are allowed
*/
public function __construct($hexString)
{
$engineClass = self::resolveEngineToUse();
self::$engine = new $engineClass($hexString);
}
/**
* Encrypt the plaintext.
*
* @param string $plaintext Plaintext
*
* @return string Cipher text
*/
public function encrypt($plaintext)
{
return self::$engine->encrypt($plaintext);
}
/**
* Decrypt the cipher text.
*
* @param string $cipherText Cipher text
*
* @return bool|string Plaintext
* `false` if unable to decrypt
*
* @throws Exception
*/
public function decrypt($cipherText)
{
return self::$engine->decrypt($cipherText);
}
/**
* @param string $header
* @param string $bytes
*
* @return string
*
* @throws EnvironmentIsBrokenException
*/
public static function saveBytesToChecksummedAsciiSafeString($header, $bytes)
{
$engine = self::resolveEngineToUse();
return $engine::saveBytesToChecksummedAsciiSafeString($header, $bytes);
}
/**
* @return string
*
* @throws Exception
*/
public static function createNewRandomKey()
{
$engine = self::resolveEngineToUse();
try {
$randomKey = $engine::createNewRandomKey();
} catch (EnvironmentIsBrokenException $exception) {
$buf = $engine::randomCompat();
$randomKey = $engine::saveToAsciiSafeString($buf);
}
return $randomKey;
}
/**
* Choose which engine use regarding the OpenSSL cipher methods available.
*/
public static function resolveEngineToUse()
{
return self::ENGINE;
}
}

View File

@@ -0,0 +1,132 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use Defuse\Crypto\Crypto;
use Defuse\Crypto\Encoding;
use Defuse\Crypto\Key;
/**
* Class PhpEncryption engine for openSSL 1.0.1+.
*/
class PhpEncryptionEngineCore
{
protected $key;
/**
* PhpEncryptionCore constructor.
*
* @param string $hexString A string that only contains hexadecimal characters
* Bother upper and lower case are allowed
*/
public function __construct($hexString)
{
$this->key = self::loadFromAsciiSafeString($hexString);
}
/**
* Encrypt the plaintext.
*
* @param string $plaintext Plaintext
*
* @return string Cipher text
*/
public function encrypt($plaintext)
{
return Crypto::encrypt($plaintext, $this->key);
}
/**
* Decrypt the cipher text.
*
* @param string $cipherText Cipher text
*
* @return bool|string Plaintext
* `false` if unable to decrypt
*
* @throws Exception
*/
public function decrypt($cipherText)
{
try {
$plaintext = Crypto::decrypt($cipherText, $this->key);
} catch (Exception $e) {
if ($e instanceof Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException) {
return false;
}
throw $e;
}
return $plaintext;
}
/**
* @param string $header
* @param string $bytes
*
* @return string
*
* @throws Defuse\Crypto\Exception\EnvironmentIsBrokenException
*/
public static function saveBytesToChecksummedAsciiSafeString($header, $bytes)
{
return Encoding::saveBytesToChecksummedAsciiSafeString($header, $bytes);
}
/**
* @return string
*/
public static function createNewRandomKey()
{
$key = Key::createNewRandomKey();
return $key->saveToAsciiSafeString();
}
/**
* @param string $hexString
*
* @return Key
*/
public static function loadFromAsciiSafeString($hexString)
{
return Key::loadFromAsciiSafeString($hexString);
}
/**
* @return string
*
* @throws Exception
*/
public static function randomCompat()
{
$bytes = Key::KEY_BYTE_SIZE;
$secure = true;
$buf = openssl_random_pseudo_bytes($bytes, $secure);
if (
$buf !== false
&& $secure
&& mb_strlen($buf, '8bit') === $bytes
) {
return $buf;
}
throw new Exception('Could not gather sufficient random data');
}
/**
* @param string $buf
*
* @return string
*/
public static function saveToAsciiSafeString($buf)
{
return Encoding::saveBytesToChecksummedAsciiSafeString(
Key::KEY_CURRENT_VERSION,
$buf
);
}
}

View File

@@ -0,0 +1,322 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class PrestaShopBackupCore.
*/
class PrestaShopBackupCore
{
/** @var string Object id */
public $id;
/** @var string Last error messages */
public $error;
/** @var string default backup directory. */
public static $backupDir = '/backups/';
/** @var string custom backup directory. */
public $customBackupDir = null;
/** @var bool|string */
public $psBackupAll = true;
/** @var bool|string */
public $psBackupDropTable = true;
/**
* Creates a new backup object.
*
* @param string $filename Filename of the backup file
*/
public function __construct($filename = null)
{
if ($filename) {
$this->id = $this->getRealBackupPath($filename);
}
$psBackupAll = Configuration::get('PS_BACKUP_ALL');
$psBackupDropTable = Configuration::get('PS_BACKUP_DROP_TABLE');
$this->psBackupAll = $psBackupAll !== false ? $psBackupAll : true;
$this->psBackupDropTable = $psBackupDropTable !== false ? $psBackupDropTable : true;
}
/**
* you can set a different path with that function.
*
* @TODO include the prefix name
*
* @param string $dir
*
* @return bool
*/
public function setCustomBackupPath($dir)
{
$customDir = DIRECTORY_SEPARATOR . trim($dir, '/') . DIRECTORY_SEPARATOR;
if (is_dir(_PS_ADMIN_DIR_ . $customDir)) {
$this->customBackupDir = $customDir;
return true;
}
return false;
}
/**
* get the path to use for backup (customBackupDir if specified, or default).
*
* @param string $filename filename to use
*
* @return string full path
*/
public function getRealBackupPath($filename = null)
{
$backupDir = PrestaShopBackup::getBackupPath($filename);
if (!empty($this->customBackupDir)) {
$backupDir = str_replace(
_PS_ADMIN_DIR_ . self::$backupDir,
_PS_ADMIN_DIR_ . $this->customBackupDir,
$backupDir
);
if (strrpos($backupDir, DIRECTORY_SEPARATOR)) {
$backupDir .= DIRECTORY_SEPARATOR;
}
}
return $backupDir;
}
/**
* Get the full path of the backup file.
*
* @param string $filename prefix of the backup file (datetime will be the second part)
*
* @return string The full path of the backup file, or false if the backup file does not exists
*/
public static function getBackupPath($filename = '')
{
$backupdir = realpath(_PS_ADMIN_DIR_ . self::$backupDir);
if ($backupdir === false) {
throw new PrestaShopException(Context::getContext()->getTranslator()->trans('"Backup" directory does not exist.', [], 'Admin.Advparameters.Notification'));
}
// Check the realpath so we can validate the backup file is under the backup directory
if (!empty($filename)) {
$backupfile = realpath($backupdir . DIRECTORY_SEPARATOR . $filename);
} else {
$backupfile = $backupdir . DIRECTORY_SEPARATOR;
}
if ($backupfile === false || strncmp($backupdir, $backupfile, strlen($backupdir)) != 0) {
throw new PrestaShopException('Invalid backup file.');
}
return $backupfile;
}
/**
* Check if a backup file exist.
*
* @param string $filename prefix of the backup file (datetime will be the second part)
*
* @return bool true if backup file exist
*/
public static function backupExist($filename)
{
$backupdir = realpath(_PS_ADMIN_DIR_ . self::$backupDir);
if ($backupdir === false) {
throw new PrestaShopException(Context::getContext()->getTranslator()->trans('"Backup" directory does not exist.', [], 'Admin.Advparameters.Notification'));
}
return @filemtime($backupdir . DIRECTORY_SEPARATOR . $filename);
}
/**
* Get the URL used to retrieve this backup file.
*
* @return string The url used to request the backup file
*
* @deprecated As the call has been duplicated in the new Controller. Get the URL from the router instead.
*/
public function getBackupURL()
{
// Additionnal parameters (action, filename, ajax) are kept for backward compatibility, in case we disable the new controller
return Context::getContext()->link->getAdminLink(
'AdminBackup',
true,
[
'route' => 'admin_backup_download',
'downloadFileName' => basename($this->id),
],
[
'action' => 'backupContent',
'ajax' => 1,
'filename' => basename($this->id),
]
);
}
/**
* Delete the current backup file.
*
* @return bool Deletion result, true on success
*/
public function delete()
{
if (!$this->id || !unlink($this->id)) {
$this->error = Context::getContext()->getTranslator()->trans('Error deleting', [], 'Admin.Advparameters.Notification') . ' ' . ($this->id ? '"' . $this->id . '"' :
Context::getContext()->getTranslator()->trans('Invalid ID', [], 'Admin.Advparameters.Notification'));
return false;
}
return true;
}
/**
* Deletes a range of backup files.
*
* @return bool True on success
*/
public function deleteSelection(array $list)
{
foreach ($list as $file) {
$backup = new PrestaShopBackup($file);
if (!$backup->delete()) {
$this->error = $backup->error;
return false;
}
}
return true;
}
/**
* Creates a new backup file.
*
* @return bool true on successful backup
*/
public function add()
{
if (!$this->psBackupAll) {
$ignoreInsertTable = [_DB_PREFIX_ . 'connections', _DB_PREFIX_ . 'connections_page', _DB_PREFIX_
. 'connections_source', _DB_PREFIX_ . 'guest', _DB_PREFIX_ . 'statssearch',
];
} else {
$ignoreInsertTable = [];
}
// Generate some random number, to make it extra hard to guess backup file names
$rand = dechex(mt_rand(0, min(0xFFFFFFFF, mt_getrandmax())));
$date = time();
$backupfile = $this->getRealBackupPath() . $date . '-' . $rand . '.sql';
// Figure out what compression is available and open the file
if (function_exists('bzopen')) {
$backupfile .= '.bz2';
$fp = @bzopen($backupfile, 'w');
} elseif (function_exists('gzopen')) {
$backupfile .= '.gz';
$fp = @gzopen($backupfile, 'w');
} else {
$fp = @fopen($backupfile, 'wb');
}
if ($fp === false) {
echo Context::getContext()->getTranslator()->trans('Unable to create backup file', [], 'Admin.Advparameters.Notification') . ' "' . addslashes($backupfile) . '"';
return false;
}
$this->id = realpath($backupfile);
fwrite($fp, '/* Backup for ' . Tools::getHttpHost(false, false) . __PS_BASE_URI__ . "\n * at " . date('Y-m-d H:i:s', $date) . "\n */\n");
fwrite($fp, "\n" . 'SET NAMES \'utf8mb4\';');
fwrite($fp, "\n" . 'SET FOREIGN_KEY_CHECKS = 0;');
fwrite($fp, "\n" . 'SET SESSION sql_mode = \'\';' . "\n\n");
// Find all tables
$tables = Db::getInstance()->executeS('SHOW TABLES');
$found = 0;
foreach ($tables as $table) {
$table = current($table);
// Skip tables which do not start with _DB_PREFIX_
if (strlen($table) < strlen(_DB_PREFIX_) || strncmp($table, _DB_PREFIX_, strlen(_DB_PREFIX_)) != 0) {
continue;
}
// Export the table schema
$schema = Db::getInstance()->executeS('SHOW CREATE TABLE `' . $table . '`');
if (count($schema) != 1 || !isset($schema[0]['Table']) || !isset($schema[0]['Create Table'])) {
fclose($fp);
$this->delete();
echo Context::getContext()->getTranslator()->trans('An error occurred while backing up. Unable to obtain the schema of %s', [$table], 'Admin.Advparameters.Notification');
return false;
}
fwrite($fp, '/* Scheme for table ' . $schema[0]['Table'] . " */\n");
if ($this->psBackupDropTable) {
fwrite($fp, 'DROP TABLE IF EXISTS `' . $schema[0]['Table'] . '`;' . "\n");
}
fwrite($fp, $schema[0]['Create Table'] . ";\n\n");
if (!in_array($schema[0]['Table'], $ignoreInsertTable)) {
$data = Db::getInstance()->query('SELECT * FROM `' . $schema[0]['Table'] . '`');
$sizeof = Db::getInstance()->numRows();
if ($data && $sizeof > 0) {
// First we write the beginning of an insert query
fwrite($fp, 'INSERT INTO `' . $schema[0]['Table'] . "` VALUES\n");
// We start a counter, because we want to separate the queries by batches of 200 lines
$i = 1;
while ($row = Db::getInstance()->nextRow($data)) {
$s = '(';
foreach ($row as $value) {
if ($value === null) {
$s .= 'NULL,';
} else {
$s .= "'" . pSQL($value, true) . "',";
}
}
$s = rtrim($s, ',');
if ($i % 200 == 0 && $i < $sizeof) {
$s .= ");\nINSERT INTO `" . $schema[0]['Table'] . "` VALUES\n";
} elseif ($i < $sizeof) {
$s .= "),\n";
} else {
$s .= ");\n";
}
fwrite($fp, $s);
++$i;
}
}
}
++$found;
}
fclose($fp);
if ($found == 0) {
$this->delete();
echo Context::getContext()->getTranslator()->trans('No valid tables were found to backup.', [], 'Admin.Advparameters.Notification');
return false;
}
return true;
}
}

View File

@@ -0,0 +1,781 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Create a collection of ObjectModel objects.
*/
class PrestaShopCollectionCore implements Iterator, ArrayAccess, Countable
{
public const LEFT_JOIN = 1;
public const INNER_JOIN = 2;
public const LEFT_OUTER_JOIN = 3;
/**
* @var string Object class name
*/
protected $classname;
/**
* @var int
*/
protected $id_lang;
/**
* @var array Object definition
*/
protected $definition = [];
/**
* @var DbQuery
*/
protected $query;
/**
* @var array Collection of objects in an array
*/
protected $results = [];
/**
* @var bool Is current collection already hydrated
*/
protected $is_hydrated = false;
/**
* @var int Collection iterator
*/
protected $iterator = 0;
/**
* @var int Total of elements for iteration
*/
protected $total;
/**
* @var int Page number
*/
protected $page_number = 0;
/**
* @var int Size of a page
*/
protected $page_size = 0;
protected $fields = [];
protected $alias = [];
protected $alias_iterator = 0;
protected $join_list = [];
protected $association_definition = [];
public const LANG_ALIAS = 'l';
/**
* @param string $classname
* @param int $id_lang
*/
public function __construct($classname, $id_lang = null)
{
$this->classname = $classname;
$this->id_lang = $id_lang;
$this->definition = ObjectModel::getDefinition($this->classname);
if (!isset($this->definition['table'])) {
throw new PrestaShopException('Miss table in definition for class ' . $this->classname);
} elseif (!isset($this->definition['primary'])) {
throw new PrestaShopException('Miss primary in definition for class ' . $this->classname);
}
$this->query = new DbQuery();
}
/**
* Join current entity to an associated entity.
*
* @param string $association Association name
* @param string $on
* @param int $type
*
* @return $this
*/
public function join($association, $on = '', $type = null)
{
if (!$association) {
return $this;
}
if (!isset($this->join_list[$association])) {
$definition = $this->getDefinition($association);
$on = '{' . $definition['asso']['complete_field'] . '} = {' . $definition['asso']['complete_foreign_field'] . '}';
$type = self::LEFT_JOIN;
$this->join_list[$association] = [
'table' => ($definition['is_lang']) ? $definition['table'] . '_lang' : $definition['table'],
'alias' => $this->generateAlias($association),
'on' => [],
];
}
if ($on) {
$this->join_list[$association]['on'][] = $this->parseFields($on);
}
if ($type) {
$this->join_list[$association]['type'] = $type;
}
return $this;
}
/**
* Add WHERE restriction on query.
*
* @param string $field Field name
* @param string $operator List of operators : =, !=, <>, <, <=, >, >=, like, notlike, regexp, notregexp
* @param mixed $value
* @param string $method where|having
*
* @return $this
*/
public function where($field, $operator, $value, $method = 'where')
{
if ($method != 'where' && $method != 'having') {
throw new PrestaShopException('Bad method argument for where() method (should be "where" or "having")');
}
// Create WHERE clause with an array value (IN, NOT IN)
if (is_array($value)) {
switch (strtolower($operator)) {
case '=':
case 'in':
$this->query->$method($this->parseField($field) . ' IN(' . implode(', ', $this->formatValue($value, $field)) . ')');
break;
case '!=':
case '<>':
case 'notin':
$this->query->$method($this->parseField($field) . ' NOT IN(' . implode(', ', $this->formatValue($value, $field)) . ')');
break;
default:
throw new PrestaShopException('Operator not supported for array value');
}
} else {
// Create WHERE clause
switch (strtolower($operator)) {
case '=':
case '!=':
case '<>':
case '>':
case '>=':
case '<':
case '<=':
case 'like':
case 'regexp':
$this->query->$method($this->parseField($field) . ' ' . $operator . ' ' . $this->formatValue($value, $field));
break;
case 'notlike':
$this->query->$method($this->parseField($field) . ' NOT LIKE ' . $this->formatValue($value, $field));
break;
case 'notregexp':
$this->query->$method($this->parseField($field) . ' NOT REGEXP ' . $this->formatValue($value, $field));
break;
default:
throw new PrestaShopException('Operator not supported');
}
}
return $this;
}
/**
* Add WHERE restriction on query using real SQL syntax.
*
* @param string $sql
*
* @return $this
*/
public function sqlWhere($sql)
{
$this->query->where($this->parseFields($sql));
return $this;
}
/**
* Add HAVING restriction on query.
*
* @param string $field Field name
* @param string $operator List of operators : =, !=, <>, <, <=, >, >=, like, notlike, regexp, notregexp
* @param mixed $value
*
* @return $this
*/
public function having($field, $operator, $value)
{
return $this->where($field, $operator, $value, 'having');
}
/**
* Add HAVING restriction on query using real SQL syntax.
*
* @param string $sql
*
* @return $this
*/
public function sqlHaving($sql)
{
$this->query->having($this->parseFields($sql));
return $this;
}
/**
* Add ORDER BY restriction on query.
*
* @param string $field Field name
* @param string $order asc|desc
*
* @return $this
*/
public function orderBy($field, $order = 'asc')
{
$order = strtolower($order);
if ($order != 'asc' && $order != 'desc') {
throw new PrestaShopException('Order must be asc or desc');
}
$this->query->orderBy($this->parseField($field) . ' ' . $order);
return $this;
}
/**
* Add ORDER BY restriction on query using real SQL syntax.
*
* @param string $sql
*
* @return $this
*/
public function sqlOrderBy($sql)
{
$this->query->orderBy($this->parseFields($sql));
return $this;
}
/**
* Add GROUP BY restriction on query.
*
* @param string $field Field name
*
* @return $this
*/
public function groupBy($field)
{
$this->query->groupBy($this->parseField($field));
return $this;
}
/**
* Add GROUP BY restriction on query using real SQL syntax.
*
* @param string $sql
*
* @return $this
*/
public function sqlGroupBy($sql)
{
$this->query->groupBy($this->parseFields($sql));
return $this;
}
/**
* Launch sql query to create collection of objects.
*
* @param bool $display_query If true, query will be displayed (for debug purpose)
*
* @return $this
*/
public function getAll($display_query = false)
{
if ($this->is_hydrated) {
return $this;
}
$this->is_hydrated = true;
$alias = $this->generateAlias();
$this->query->from($this->definition['table'], $alias);
// If multilang, create association to lang table
if (!empty($this->definition['multilang'])) {
$this->join(self::LANG_ALIAS);
if ($this->id_lang) {
$this->where(self::LANG_ALIAS . '.id_lang', '=', $this->id_lang);
}
if (!empty($this->definition['multilang_shop'])) {
$this->sqlWhere($this->join_list[self::LANG_ALIAS]['alias'] . '.`id_shop` = ' . (int) Context::getContext()->shop->id);
}
}
// Add join clause
foreach ($this->join_list as $data) {
$on = '(' . implode(') AND (', $data['on']) . ')';
switch ($data['type']) {
case self::LEFT_JOIN:
$this->query->leftJoin($data['table'], $data['alias'], $on);
break;
case self::INNER_JOIN:
$this->query->innerJoin($data['table'], $data['alias'], $on);
break;
case self::LEFT_OUTER_JOIN:
$this->query->leftOuterJoin($data['table'], $data['alias'], $on);
break;
}
}
// All limit clause
if ($this->page_size) {
$this->query->limit($this->page_size, $this->page_number * $this->page_size);
}
// Shall we display query for debug ?
if ($display_query) {
echo $this->query . '<br />';
}
$this->results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($this->query);
if ($this->results && is_array($this->results)) {
$this->results = ObjectModel::hydrateCollection($this->classname, $this->results, $this->id_lang);
}
return $this;
}
/**
* Retrieve the first result.
*
* @return ObjectModel|bool
*/
public function getFirst()
{
$this->getAll();
if (!count($this)) {
return false;
}
return $this[0];
}
/**
* Retrieve the last result.
*
* @return ObjectModel|false
*/
public function getLast()
{
$this->getAll();
if (!count($this)) {
return false;
}
return $this[count($this) - 1];
}
/**
* Get results array.
*
* @return array
*/
public function getResults()
{
$this->getAll();
return $this->results;
}
/**
* This method is called when a foreach begin.
*
* @see Iterator::rewind()
*/
public function rewind(): void
{
$this->getAll();
$this->results = array_merge($this->results);
$this->iterator = 0;
$this->total = count($this->results);
}
/**
* Get current result.
*
* @see Iterator::current()
*
* @return ObjectModel
*/
#[ReturnTypeWillChange]
public function current()
{
return isset($this->results[$this->iterator]) ? $this->results[$this->iterator] : null;
}
/**
* Check if there is a current result.
*
* @see Iterator::valid()
*
* @return bool
*/
public function valid(): bool
{
return $this->iterator < $this->total;
}
/**
* Get current result index.
*
* @see Iterator::key()
*
* @return int
*/
#[ReturnTypeWillChange]
public function key()
{
return $this->iterator;
}
/**
* Go to next result.
*
* @see Iterator::next()
*/
public function next(): void
{
++$this->iterator;
}
/**
* Get total of results.
*
* @see Countable::count()
*
* @return int
*/
public function count(): int
{
$this->getAll();
return count($this->results);
}
/**
* Check if a result exist.
*
* @see ArrayAccess::offsetExists()
*
* @param mixed $offset
*
* @return bool
*/
public function offsetExists($offset): bool
{
$this->getAll();
return isset($this->results[$offset]);
}
/**
* Get a result by offset.
*
* @see ArrayAccess::offsetGet()
*
* @param mixed $offset
*
* @return ObjectModel
*/
#[ReturnTypeWillChange]
public function offsetGet($offset)
{
$this->getAll();
if (!isset($this->results[$offset])) {
throw new PrestaShopException('Unknown offset ' . $offset . ' for collection ' . $this->classname);
}
return $this->results[$offset];
}
/**
* Add an element in the collection.
*
* @see ArrayAccess::offsetSet()
*
* @param mixed $offset
* @param ObjectModel $value
*/
public function offsetSet($offset, $value): void
{
if (!$value instanceof $this->classname) {
throw new PrestaShopException('You cannot add an element which is not an instance of ' . $this->classname);
}
$this->getAll();
if (null === $offset) {
$this->results[] = $value;
} else {
$this->results[$offset] = $value;
}
}
/**
* Delete an element from the collection.
*
* @see ArrayAccess::offsetUnset()
*
* @param mixed $offset
*/
public function offsetUnset($offset): void
{
$this->getAll();
unset($this->results[$offset]);
}
/**
* Get definition of an association.
*
* @param string $association
*
* @return array
*/
protected function getDefinition($association)
{
if (!$association) {
return $this->definition;
}
if (!isset($this->association_definition[$association])) {
$definition = $this->definition;
$split = explode('.', $association);
$is_lang = false;
$asso = '';
for ($i = 0, $total_association = count($split); $i < $total_association; ++$i) {
$asso = $split[$i];
// Check is current association exists in current definition
if (!isset($definition['associations'][$asso])) {
throw new PrestaShopException('Association ' . $asso . ' not found for class ' . $this->definition['classname']);
}
$current_def = $definition['associations'][$asso];
// Special case for lang alias
if ($asso == self::LANG_ALIAS) {
$is_lang = true;
break;
}
$classname = (isset($current_def['object'])) ? $current_def['object'] : Tools::toCamelCase($asso, true);
$definition = ObjectModel::getDefinition($classname);
}
// Get definition of associated entity and add information on current association
$current_def['name'] = $asso;
if (!isset($current_def['object'])) {
$current_def['object'] = Tools::toCamelCase($asso, true);
}
if (!isset($current_def['field'])) {
$current_def['field'] = 'id_' . $asso;
}
if (!isset($current_def['foreign_field'])) {
$current_def['foreign_field'] = 'id_' . $asso;
}
if ($total_association > 1) {
unset($split[$total_association - 1]);
$current_def['complete_field'] = implode('.', $split) . '.' . $current_def['field'];
} else {
$current_def['complete_field'] = $current_def['field'];
}
$current_def['complete_foreign_field'] = $association . '.' . $current_def['foreign_field'];
$definition['is_lang'] = $is_lang;
$definition['asso'] = $current_def;
$this->association_definition[$association] = $definition;
} else {
$definition = $this->association_definition[$association];
}
return $definition;
}
/**
* Parse all fields with {field} syntax in a string.
*
* @param string $str
*
* @return string
*/
protected function parseFields($str)
{
preg_match_all('#\{(([a-z0-9_]+\.)*[a-z0-9_]+)\}#i', $str, $m);
for ($i = 0, $total = count($m[0]); $i < $total; ++$i) {
$str = str_replace($m[0][$i], $this->parseField($m[1][$i]), $str);
}
return $str;
}
/**
* Replace a field with its SQL version (E.g. manufacturer.name with a2.name).
*
* @param string $field Field name
*
* @return string
*/
protected function parseField($field)
{
$info = $this->getFieldInfo($field);
return $info['alias'] . '.`' . $info['name'] . '`';
}
/**
* Format a value with the type of the given field.
*
* @param mixed $value
* @param string $field Field name
*
* @return mixed
*/
protected function formatValue($value, $field)
{
$info = $this->getFieldInfo($field);
if (is_array($value)) {
$results = [];
foreach ($value as $item) {
$results[] = ObjectModel::formatValue($item, $info['type'], true);
}
return $results;
}
return ObjectModel::formatValue($value, $info['type'], true);
}
/**
* Obtain some information on a field (alias, name, type, etc.).
*
* @param string $field Field name
*
* @return array
*/
protected function getFieldInfo($field)
{
if (!isset($this->fields[$field])) {
$split = explode('.', $field);
$total = count($split);
if ($total > 1) {
$fieldname = $split[$total - 1];
unset($split[$total - 1]);
$association = implode('.', $split);
} else {
$fieldname = $field;
$association = '';
}
$definition = $this->getDefinition($association);
if ($association && !isset($this->join_list[$association])) {
$this->join($association);
}
if ($fieldname == $definition['primary'] || (!empty($definition['is_lang']) && $fieldname == 'id_lang')) {
$type = ObjectModel::TYPE_INT;
} else {
// Test if field exists
if (!isset($definition['fields'][$fieldname])) {
throw new PrestaShopException('Field ' . $fieldname . ' not found in class ' . $definition['classname']);
}
// Test field validity for language fields
if (empty($definition['is_lang']) && !empty($definition['fields'][$fieldname]['lang'])) {
throw new PrestaShopException('Field ' . $fieldname . ' is declared as lang field but is used in non multilang context');
} elseif (!empty($definition['is_lang']) && empty($definition['fields'][$fieldname]['lang'])) {
throw new PrestaShopException('Field ' . $fieldname . ' is not declared as lang field but is used in multilang context');
}
$type = $definition['fields'][$fieldname]['type'];
}
$this->fields[$field] = [
'name' => $fieldname,
'association' => $association,
'alias' => $this->generateAlias($association),
'type' => $type,
];
}
return $this->fields[$field];
}
/**
* Set the page number.
*
* @param int $page_number
*
* @return $this
*/
public function setPageNumber($page_number)
{
$page_number = (int) $page_number;
if ($page_number > 0) {
--$page_number;
}
$this->page_number = $page_number;
return $this;
}
/**
* Set the nuber of item per page.
*
* @param int $page_size
*
* @return $this
*/
public function setPageSize($page_size)
{
$this->page_size = (int) $page_size;
return $this;
}
/**
* Generate uniq alias from association name.
*
* @param string $association Use empty association for alias on current table
*
* @return string
*/
protected function generateAlias($association = '')
{
if (!isset($this->alias[$association])) {
$this->alias[$association] = 'a' . $this->alias_iterator++;
}
return $this->alias[$association];
}
}

View File

@@ -0,0 +1,247 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShopBundle\Form\Admin\Type\FormattedTextareaType;
/**
* Class PrestaShopLoggerCore.
*/
class PrestaShopLoggerCore extends ObjectModel
{
/**
* List of log level types.
*/
public const LOG_SEVERITY_LEVEL_DEBUG = 0;
public const LOG_SEVERITY_LEVEL_INFORMATIVE = 1;
public const LOG_SEVERITY_LEVEL_WARNING = 2;
public const LOG_SEVERITY_LEVEL_ERROR = 3;
public const LOG_SEVERITY_LEVEL_MAJOR = 4;
/** @var int Log id */
public $id_log;
/** @var int Log severity */
public $severity;
/** @var int Error code */
public $error_code;
/** @var string Message */
public $message;
/** @var string Object type (eg. Order, Customer...) */
public $object_type;
/** @var int Object ID */
public $object_id;
/** @var int Employee ID */
public $id_employee;
/** @var string Object creation date */
public $date_add;
/** @var string Object last modification date */
public $date_upd;
/** @var int|null Shop ID */
public $id_shop;
/** @var int|null Shop group ID */
public $id_shop_group;
/** @var int|null Language ID */
public $id_lang;
/** @var bool In all shops */
public $in_all_shops;
/** @var string|null */
public $hash;
protected static int $minLevelInDb;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'log',
'primary' => 'id_log',
'fields' => [
'severity' => ['type' => self::TYPE_INT, 'validate' => 'isInt', 'required' => true],
'error_code' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
'message' => ['type' => self::TYPE_STRING, 'validate' => 'isString', 'required' => true, 'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4],
'object_id' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
'id_shop' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'allow_null' => true],
'id_shop_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'allow_null' => true],
'id_lang' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'allow_null' => true],
'in_all_shops' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'id_employee' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
'object_type' => ['type' => self::TYPE_STRING, 'validate' => 'isValidObjectClassName', 'size' => 32],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
],
];
protected static $is_present = [];
/**
* Send e-mail to the shop owner only if the minimal severity level has been reached.
*
* @param PrestaShopLogger $log
*/
public static function sendByMail($log)
{
$config_severity = (int) Configuration::get('PS_LOGS_BY_EMAIL');
if (!empty($config_severity) && $config_severity <= (int) $log->severity) {
$to = array_map('trim', explode(',', Configuration::get('PS_LOGS_EMAIL_RECEIVERS')));
$language = new Language((int) Configuration::get('PS_LANG_DEFAULT'));
Mail::Send(
(int) Configuration::get('PS_LANG_DEFAULT'),
'log_alert',
Context::getContext()->getTranslator()->trans(
'Log: You have a new alert from your shop',
[],
'Emails.Subject',
$language->locale
),
[],
$to
);
}
}
/**
* add a log item to the database and send a mail if configured for this $severity.
*
* @param string $message the log message
* @param int $severity
* @param int $errorCode
* @param string $objectType
* @param int $objectId
* @param bool $allowDuplicate if set to true, can log several time the same information (not recommended)
*
* @return bool true if succeed
*/
public static function addLog($message, $severity = 1, $errorCode = null, $objectType = null, $objectId = null, $allowDuplicate = false, $idEmployee = null)
{
// Not all logs are relevant in DB so we filter them based on the configuration PS_MIN_LOGGER_LEVEL_IN_DB
if ($severity < self::getMinimumLevelInDB()) {
return false;
}
$log = new PrestaShopLogger();
$log->severity = (int) $severity;
$log->error_code = (int) $errorCode;
$log->message = $message;
$log->date_add = date('Y-m-d H:i:s');
$log->date_upd = date('Y-m-d H:i:s');
$context = Context::getContext();
if ($idEmployee === null && isset($context->employee->id)) {
$idEmployee = $context->employee->id;
}
if ($idEmployee !== null) {
$log->id_employee = (int) $idEmployee;
}
if (!empty($objectType)) {
$log->object_type = $objectType;
if (!empty($objectId)) {
$log->object_id = (int) $objectId;
}
}
$log->id_lang = $context->language ? (int) $context->language->id : null;
$log->in_all_shops = Shop::getContext() == Shop::CONTEXT_ALL;
$log->id_shop = Shop::getContext() == Shop::CONTEXT_SHOP ? (int) $context->shop->getContextualShopId() : null;
$log->id_shop_group = Shop::getContext() == Shop::CONTEXT_GROUP ? (int) $context->shop->getContextShopGroupID() : null;
if ($objectType != 'MailerMessage') {
PrestaShopLogger::sendByMail($log);
}
if ($allowDuplicate || !$log->isPresent()) {
$res = $log->add();
if ($res) {
self::$is_present[$log->getHash()] = isset(self::$is_present[$log->getHash()]) ? self::$is_present[$log->getHash()] + 1 : 1;
return true;
}
}
return false;
}
/**
* @return string hash
*/
public function getHash()
{
if (empty($this->hash)) {
$this->hash = md5(
$this->message .
$this->severity .
$this->error_code .
$this->object_type .
$this->object_id .
$this->id_shop .
$this->id_shop_group .
$this->id_lang .
$this->in_all_shops
);
}
return $this->hash;
}
public static function eraseAllLogs()
{
return Db::getInstance()->execute('TRUNCATE TABLE ' . _DB_PREFIX_ . 'log');
}
/**
* check if this log message already exists in database.
*
* @return bool true if exists
*/
protected function isPresent()
{
if (!isset(self::$is_present[md5($this->message)])) {
self::$is_present[$this->getHash()] = Db::getInstance()->getValue(
(new DbQuery())
->select('COUNT(*)')
->from('log', 'l')
->where('message = "' . pSQL($this->message) . '"')
->where('severity = ' . (int) $this->severity)
->where('error_code = ' . (int) $this->error_code)
->where('object_type = "' . pSQL($this->object_type) . '"')
->where('object_id = ' . (int) $this->object_id)
->where('id_shop = ' . (int) $this->id_shop)
->where('id_shop_group = ' . (int) $this->id_shop_group)
->where('id_lang = ' . (int) $this->id_lang)
->where('in_all_shops = ' . (int) $this->in_all_shops)
);
}
return self::$is_present[$this->getHash()];
}
protected static function getMinimumLevelInDB(): int
{
if (!isset(self::$minLevelInDb)) {
try {
self::$minLevelInDb = (int) Configuration::get('PS_MIN_LOGGER_LEVEL_IN_DB', null, null, null, self::LOG_SEVERITY_LEVEL_INFORMATIVE);
} catch (Throwable) {
self::$minLevelInDb = self::LOG_SEVERITY_LEVEL_INFORMATIVE;
}
}
return self::$minLevelInDb;
}
}

8113
classes/Product.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,203 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchContext;
/**
* This class is responsible for enriching product data by all required fields
* in a performant way, before it goes into ProductLazyArray or ProductListingLazyArray.
*
* If you want to enrich a whole list of products, use assembleProducts method to get the data in one query.
*
* Currently, the data is passing through Product::getProductProperties also, but this step should be removed
* and all data from getProductProperties loaded on demand in the lazy arrays.
*/
class ProductAssemblerCore
{
private $context;
private $searchContext;
/**
* ProductAssemblerCore constructor.
*
* @param Context $context
*/
public function __construct(Context $context)
{
$this->context = $context;
$this->searchContext = new ProductSearchContext($context);
}
/**
* Add missing product fields.
*
* @param array $rawProduct
*
* @return array
*
* @throws PrestaShopDatabaseException
*/
private function addMissingProductFields(array $rawProduct): array
{
// If there is no ID product provided, return the original data
if (empty($rawProduct['id_product'])) {
return $rawProduct;
}
$sql = $this->getSqlQueryProductFields([(int) $rawProduct['id_product']]);
$rows = Db::getInstance()->executeS($sql);
if (empty($rows)) {
return $rawProduct;
}
return array_merge($rows[0], $rawProduct);
}
/**
* Add missing product fields to multiple products.
*
* @param array $rawProducts
*
* @return array
*
* @throws PrestaShopDatabaseException
*/
private function addMissingProductFieldsForMultipleProducts(array $rawProducts): array
{
// Get product IDs we want to retrieve from database
$productIds = array_column($rawProducts, 'id_product');
// If there were no product IDs provided or somebody passed an empty array,
// return the original data
if (empty($productIds)) {
return $rawProducts;
}
// Retrieve data and reassign them to new array by their key
$productData = [];
$sql = $this->getSqlQueryProductFields($productIds);
$rows = Db::getInstance()->executeS($sql);
foreach ($rows as $row) {
$productData[(int) $row['id_product']] = $row;
}
// Use this data to enrich the products and return it
foreach ($rawProducts as &$rawProduct) {
if (isset($productData[$rawProduct['id_product']])) {
$rawProduct = array_merge($productData[$rawProduct['id_product']], $rawProduct);
}
}
return $rawProducts;
}
/**
* Return the SQL query to get all product fields.
*
* @param array $productIds
*
* @return string
*/
private function getSqlQueryProductFields(array $productIds): string
{
// Get basic configuration
$idShop = $this->searchContext->getIdShop();
$idShopGroup = $this->searchContext->getIdShopGroup();
$isStockSharingBetweenShopGroupEnabled = $this->searchContext->isStockSharingBetweenShopGroupEnabled();
$idLang = $this->searchContext->getIdLang();
$prefix = _DB_PREFIX_;
$nbDaysNewProduct = (int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT');
if (!Validate::isUnsignedInt($nbDaysNewProduct)) {
$nbDaysNewProduct = 20;
}
$now = date('Y-m-d') . ' 00:00:00';
$sql = "SELECT
p.*,
ps.*,
pl.*,
sa.out_of_stock,
IFNULL(sa.quantity, 0) as quantity,
(DATEDIFF(
p.`date_add`,
DATE_SUB(
'$now',
INTERVAL $nbDaysNewProduct DAY
)
) > 0) as new
FROM {$prefix}product p
LEFT JOIN {$prefix}product_lang pl
ON pl.id_product = p.id_product
AND pl.id_shop = $idShop
AND pl.id_lang = $idLang
LEFT JOIN {$prefix}stock_available sa ";
if ($isStockSharingBetweenShopGroupEnabled) {
$sql .= " ON sa.id_product = p.id_product
AND sa.id_shop = 0
AND sa.id_product_attribute = 0
AND sa.id_shop_group = $idShopGroup ";
} else {
$sql .= " ON sa.id_product = p.id_product
AND sa.id_product_attribute = 0
AND sa.id_shop = $idShop ";
}
$sql .= "LEFT JOIN {$prefix}product_shop ps
ON ps.id_product = p.id_product
AND ps.id_shop = $idShop
WHERE p.id_product IN (" . implode(',', $productIds) . ')';
return $sql;
}
/**
* Get basic product data for single product.
* The only required property is id_product.
* If some data were already provided in $rawProduct, it won't be overwritten.
*
* @param array $rawProduct
*
* @return mixed
*
* @throws PrestaShopDatabaseException
*/
public function assembleProduct(array $rawProduct)
{
$enrichedProduct = $this->addMissingProductFields($rawProduct);
return Product::getProductProperties(
$this->searchContext->getIdLang(),
$enrichedProduct,
$this->context
);
}
/**
* Get basic product data for multiple products.
* The only required property for each product is id_product.
* If some data were already provided in $rawProducts, it won't be overwritten.
*
* @param array $rawProducts Array with multiple products
*
* @return mixed
*
* @throws PrestaShopDatabaseException
*/
public function assembleProducts(array $rawProducts)
{
$enrichedProducts = $this->addMissingProductFieldsForMultipleProducts($rawProducts);
foreach ($enrichedProducts as &$product) {
$product = Product::getProductProperties(
$this->searchContext->getIdLang(),
$product,
$this->context
);
}
return $enrichedProducts;
}
}

View File

@@ -0,0 +1,399 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class ProductAttributeCore.
*/
class ProductAttributeCore extends ObjectModel
{
/** @var int Group id which attribute belongs */
public $id_attribute_group;
/** @var string|string[] Name */
public $name;
/** @var string */
public $color;
/** @var int */
public $position;
/** @todo Find type */
public $default;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'attribute',
'primary' => 'id_attribute',
'multilang' => true,
'fields' => [
'id_attribute_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'color' => ['type' => self::TYPE_STRING, 'validate' => 'isColor', 'size' => 32],
'position' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
/* Lang fields */
'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 128],
],
];
/** @var string */
protected $image_dir = _PS_COL_IMG_DIR_;
/** @var array Web service parameters */
protected $webserviceParameters = [
'objectsNodeName' => 'product_option_values',
'objectNodeName' => 'product_option_value',
'fields' => [
'id_attribute_group' => ['xlink_resource' => 'product_options'],
],
];
/**
* AttributeCore constructor.
*
* @param int|null $id Attribute ID
* @param int|null $idLang Language ID
* @param int|null $idShop Shop ID
*/
public function __construct($id = null, $idLang = null, $idShop = null)
{
parent::__construct($id, $idLang, $idShop);
$this->image_dir = _PS_COL_IMG_DIR_;
}
/**
* @see ObjectModel::delete()
*/
public function delete()
{
if (!$this->hasMultishopEntries() || Shop::getContext() == Shop::CONTEXT_ALL) {
$result = Db::getInstance()->executeS('SELECT id_product_attribute FROM ' . _DB_PREFIX_ . 'product_attribute_combination WHERE id_attribute = ' . (int) $this->id);
$products = [];
foreach ($result as $row) {
$combination = new Combination($row['id_product_attribute']);
$newRequest = Db::getInstance()->executeS('SELECT id_product, default_on FROM ' . _DB_PREFIX_ . 'product_attribute WHERE id_product_attribute = ' . (int) $row['id_product_attribute']);
foreach ($newRequest as $value) {
if ($value['default_on'] == 1) {
$products[] = $value['id_product'];
}
}
$combination->delete();
}
foreach ($products as $product) {
$result = Db::getInstance()->executeS('SELECT id_product_attribute FROM ' . _DB_PREFIX_ . 'product_attribute WHERE id_product = ' . (int) $product . ' LIMIT 1');
foreach ($result as $row) {
if (Validate::isLoadedObject($product = new Product((int) $product))) {
$product->deleteDefaultAttributes();
$product->setDefaultAttribute($row['id_product_attribute']);
}
}
}
// Delete associated restrictions on cart rules
CartRule::cleanProductRuleIntegrity('attributes', $this->id);
/* Reinitializing position */
$this->cleanPositions((int) $this->id_attribute_group);
}
$return = parent::delete();
if ($return) {
Hook::exec('actionAttributeDelete', ['id_attribute' => $this->id]);
}
return $return;
}
/**
* @see ObjectModel::update()
*/
public function update($nullValues = false)
{
$return = parent::update($nullValues);
if ($return) {
Hook::exec('actionAttributeSave', ['id_attribute' => $this->id]);
}
return $return;
}
/**
* Adds current ProductAttribute as a new Object to the database.
*
* @param bool $autoDate Automatically set `date_upd` and `date_add` column
* @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
*
* @return bool Whether the ProductAttribute has been successfully added
*
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
*/
public function add($autoDate = true, $nullValues = false)
{
if ($this->position <= 0) {
$this->position = static::getHigherPosition($this->id_attribute_group) + 1;
}
$return = parent::add($autoDate, $nullValues);
if ($return) {
Hook::exec('actionAttributeSave', ['id_attribute' => $this->id]);
}
return $return;
}
/**
* Get all attributes for a given language.
*
* @param int $idLang Language ID
* @param bool $notNull Get only not null fields if true
*
* @return array Attributes
*/
public static function getAttributes($idLang, $notNull = false)
{
if (!Combination::isFeatureActive()) {
return [];
}
return Db::getInstance()->executeS('
SELECT DISTINCT ag.*, agl.*, a.`id_attribute`, al.`name`, agl.`name` AS `attribute_group`
FROM `' . _DB_PREFIX_ . 'attribute_group` ag
LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl
ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) $idLang . ')
LEFT JOIN `' . _DB_PREFIX_ . 'attribute` a
ON a.`id_attribute_group` = ag.`id_attribute_group`
LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al
ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) $idLang . ')
' . Shop::addSqlAssociation('attribute_group', 'ag') . '
' . Shop::addSqlAssociation('attribute', 'a') . '
' . ($notNull ? 'WHERE a.`id_attribute` IS NOT NULL AND al.`name` IS NOT NULL AND agl.`id_attribute_group` IS NOT NULL' : '') . '
ORDER BY agl.`name` ASC, a.`position` ASC
');
}
/**
* Check if the given name is an Attribute within the given AttributeGroup.
*
* @param int $idAttributeGroup AttributeGroup
* @param string $name Attribute name
* @param int $idLang Language ID
*
* @return array|bool
*/
public static function isAttribute($idAttributeGroup, $name, $idLang)
{
if (!Combination::isFeatureActive()) {
return [];
}
$result = Db::getInstance()->getValue('
SELECT COUNT(*)
FROM `' . _DB_PREFIX_ . 'attribute_group` ag
LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl
ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) $idLang . ')
LEFT JOIN `' . _DB_PREFIX_ . 'attribute` a
ON a.`id_attribute_group` = ag.`id_attribute_group`
LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al
ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) $idLang . ')
' . Shop::addSqlAssociation('attribute_group', 'ag') . '
' . Shop::addSqlAssociation('attribute', 'a') . '
WHERE al.`name` = \'' . pSQL($name) . '\' AND ag.`id_attribute_group` = ' . (int) $idAttributeGroup . '
ORDER BY agl.`name` ASC, a.`position` ASC
');
return (int) $result > 0;
}
/**
* Get quantity for a given attribute combination
* Check if quantity is enough to serve the customer.
*
* @param int $idProductAttribute Product attribute combination id
* @param int $qty Quantity needed
* @param Shop $shop Shop
*
* @return bool Quantity is available or not
*/
public static function checkAttributeQty($idProductAttribute, $qty, ?Shop $shop = null)
{
if (!$shop) {
$shop = Context::getContext()->shop;
}
$result = Hook::exec(
'actionCheckAttributeQuantity',
[
'id_product_attribute' => $idProductAttribute,
'qty' => $qty,
'shop' => $shop,
],
null,
false,
true,
false,
null,
true
);
if (!is_int($result)) {
$result = StockAvailable::getQuantityAvailableByProduct(null, (int) $idProductAttribute, $shop->id);
}
return $result && $qty <= $result;
}
/**
* Return true if the Attribute is a color.
*
* @return bool|int Color is the attribute type
*/
public function isColorAttribute()
{
if (!Db::getInstance()->getRow('
SELECT `group_type`
FROM `' . _DB_PREFIX_ . 'attribute_group`
WHERE `id_attribute_group` = (
SELECT `id_attribute_group`
FROM `' . _DB_PREFIX_ . 'attribute`
WHERE `id_attribute` = ' . (int) $this->id . ')
AND group_type = \'color\'')) {
return false;
}
return Db::getInstance()->numRows();
}
/**
* Get minimal quantity for product with attributes quantity.
*
* @param int $idProductAttribute Product Attribute ID
*
* @return mixed Minimal quantity or false if no result
*/
public static function getAttributeMinimalQty($idProductAttribute)
{
$minimalQuantity = Db::getInstance()->getValue(
'
SELECT `minimal_quantity`
FROM `' . _DB_PREFIX_ . 'product_attribute_shop` pas
WHERE `id_shop` = ' . (int) Context::getContext()->shop->id . '
AND `id_product_attribute` = ' . (int) $idProductAttribute
);
if ($minimalQuantity > 1) {
return (int) $minimalQuantity;
}
return false;
}
/**
* Move an attribute inside its group.
*
* @param bool $direction Up (1) or Down (0)
* @param int $position Current position of the attribute
*
* @return bool Update result
*/
public function updatePosition($direction, $position)
{
if (!$idAttributeGroup = (int) Tools::getValue('id_attribute_group')) {
$idAttributeGroup = (int) $this->id_attribute_group;
}
$sql = '
SELECT a.`id_attribute`, a.`position`, a.`id_attribute_group`
FROM `' . _DB_PREFIX_ . 'attribute` a
WHERE a.`id_attribute_group` = ' . (int) $idAttributeGroup . '
ORDER BY a.`position` ASC';
if (!$res = Db::getInstance()->executeS($sql)) {
return false;
}
foreach ($res as $attribute) {
if ((int) $attribute['id_attribute'] == (int) $this->id) {
$movedAttribute = $attribute;
}
}
if (!isset($movedAttribute)) {
return false;
}
// < and > statements rather than BETWEEN operator
// since BETWEEN is treated differently according to databases
$res1 = Db::getInstance()->execute(
'
UPDATE `' . _DB_PREFIX_ . 'attribute`
SET `position`= `position` ' . ($direction ? '- 1' : '+ 1') . '
WHERE `position`
' . ($direction
? '> ' . (int) $movedAttribute['position'] . ' AND `position` <= ' . (int) $position
: '< ' . (int) $movedAttribute['position'] . ' AND `position` >= ' . (int) $position) . '
AND `id_attribute_group`=' . (int) $movedAttribute['id_attribute_group']
);
$res2 = Db::getInstance()->execute(
'
UPDATE `' . _DB_PREFIX_ . 'attribute`
SET `position` = ' . (int) $position . '
WHERE `id_attribute` = ' . (int) $movedAttribute['id_attribute'] . '
AND `id_attribute_group`=' . (int) $movedAttribute['id_attribute_group']
);
return $res1 && $res2;
}
/**
* Reorder the attribute position within the Attribute group.
* Call this method after deleting an attribute from a group.
*
* @param int $idAttributeGroup Attribute group ID
* @param bool $useLastAttribute
*
* @return bool Whether the result was successfully updated
*/
public function cleanPositions($idAttributeGroup, $useLastAttribute = true)
{
Db::getInstance()->execute('SET @i = -1', false);
$sql = 'UPDATE `' . _DB_PREFIX_ . 'attribute` SET `position` = @i:=@i+1 WHERE';
if ($useLastAttribute) {
$sql .= ' `id_attribute` != ' . (int) $this->id . ' AND';
}
$sql .= ' `id_attribute_group` = ' . (int) $idAttributeGroup . ' ORDER BY `position` ASC';
return Db::getInstance()->execute($sql);
}
/**
* get highest position.
*
* Get the highest attribute position from a group attribute
*
* @param int $idAttributeGroup AttributeGroup ID
*
* @return int $position Position
*
* @todo: Shouldn't this be called getHighestPosition instead?
*/
public static function getHigherPosition($idAttributeGroup)
{
$sql = 'SELECT MAX(`position`)
FROM `' . _DB_PREFIX_ . 'attribute`
WHERE id_attribute_group = ' . (int) $idAttributeGroup;
$position = Db::getInstance()->getValue($sql);
return (is_numeric($position)) ? $position : -1;
}
}

310
classes/ProductDownload.php Normal file
View File

@@ -0,0 +1,310 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Core\Domain\Product\VirtualProductFile\VirtualProductFileSettings;
/**
* Class ProductDownloadCore.
*/
class ProductDownloadCore extends ObjectModel
{
/** @var int Product id which download belongs */
public $id_product;
/** @var string DisplayFilename the name which appear */
public $display_filename;
/** @var string PhysicallyFilename the name of the file on hard disk */
public $filename;
/** @var string DateDeposit when the file is upload */
public $date_add;
/** @var string DateExpiration deadline of the file */
public $date_expiration;
/** @var int NbDaysAccessible how many days the customer can access to file */
public $nb_days_accessible;
/** @var int NbDownloadable how many time the customer can download the file */
public $nb_downloadable;
/** @var bool Active if file is accessible or not */
public $active = true;
/** @var bool is_shareable indicates whether the product can be shared */
public $is_shareable = false;
protected static $_productIds = [];
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'product_download',
'primary' => 'id_product_download',
'fields' => [
'id_product' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'display_filename' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => VirtualProductFileSettings::MAX_DISPLAY_FILENAME_LENGTH],
'filename' => ['type' => self::TYPE_STRING, 'validate' => 'isSha1', 'size' => VirtualProductFileSettings::MAX_FILENAME_LENGTH],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'date_expiration' => ['type' => self::TYPE_DATE, 'validate' => 'isDateOrNull'],
'nb_days_accessible' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'size' => 10],
'nb_downloadable' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'size' => 10],
'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'is_shareable' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
],
];
/**
* Build a virtual product.
*
* @param int $idProductDownload Existing productDownload id in order to load object (optional)
*/
public function __construct($idProductDownload = null)
{
parent::__construct($idProductDownload);
// @TODO check if the file is present on hard drive
}
/**
* @see ObjectModel::getFields()
*
* @return array
*/
public function getFields()
{
$fields = parent::getFields();
if (!$fields['date_expiration']) {
$fields['date_expiration'] = '0000-00-00 00:00:00';
}
return $fields;
}
public function add($autoDate = true, $nullValues = false)
{
return (bool) parent::add($autoDate, $nullValues);
}
public function update($nullValues = false)
{
if (parent::update($nullValues)) {
// Refresh cache of feature detachable because the row can be deactive
// Configuration::updateGlobalValue('PS_VIRTUAL_PROD_FEATURE_ACTIVE', ProductDownload::isCurrentlyUsed($this->def['table'], true));
return true;
}
return false;
}
public function delete($deleteFile = false)
{
$result = parent::delete();
if ($result && $deleteFile) {
return $this->deleteFile();
}
return $result;
}
/**
* Delete the file.
*
* @param int $idProductDownload : if we need to delete a specific product attribute file
*
* @return bool
*/
public function deleteFile($idProductDownload = null)
{
if (!$this->checkFile()) {
return false;
}
return unlink(_PS_DOWNLOAD_DIR_ . $this->filename)
&& Db::getInstance()->delete('product_download', 'id_product_download = ' . (int) $idProductDownload);
}
/**
* Check if file exists.
*
* @return bool
*/
public function checkFile()
{
if (!$this->filename) {
return false;
}
return file_exists(_PS_DOWNLOAD_DIR_ . $this->filename);
}
/**
* Check if download repository is writable.
*
* @return bool
*/
public static function checkWritableDir()
{
return is_writable(_PS_DOWNLOAD_DIR_);
}
/**
* Return the id_product_download from an id_product.
*
* @param int $idProduct Product the id
* @param bool $active
*
* @return bool|int Product the id for this virtual product
*/
public static function getIdFromIdProduct($idProduct, $active = true)
{
if (!ProductDownload::isFeatureActive()) {
return false;
}
self::$_productIds[$idProduct] = (int) Db::getInstance()->getValue('
SELECT `id_product_download`
FROM `' . _DB_PREFIX_ . 'product_download`
WHERE `id_product` = ' . (int) $idProduct . '
' . ($active ? ' AND `active` = 1' : '') . '
ORDER BY `id_product_download` DESC');
return self::$_productIds[$idProduct];
}
/**
* Return the display filename from a physical filename.
*
* @param string $filename Filename physically
*
* @return int Product the id for this virtual product
*/
public static function getIdFromFilename($filename)
{
return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
'SELECT `id_product_download`
FROM `' . _DB_PREFIX_ . 'product_download`
WHERE `filename` = \'' . pSQL($filename) . '\''
);
}
/**
* Return the filename from a Product ID.
*
* @param int $idProduct Product ID
*
* @return string Filename the filename for this virtual product
*/
public static function getFilenameFromIdProduct($idProduct)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
SELECT `filename`
FROM `' . _DB_PREFIX_ . 'product_download`
WHERE `id_product` = ' . (int) $idProduct . '
AND `active` = 1
');
}
/**
* Return the display filename from a physical filename.
*
* @param string $filename Filename physically
*
* @return string Filename the display filename for this virtual product
*/
public static function getFilenameFromFilename($filename)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
SELECT `display_filename`
FROM `' . _DB_PREFIX_ . 'product_download`
WHERE `filename` = \'' . pSQL($filename) . '\'');
}
/**
* Return text link.
*
* @param string|false $hash hash code in table order detail (optional)
*
* @return string Html all the code for print a link to the file
*/
public function getTextLink($hash = false)
{
$key = $this->filename . '-' . ($hash ? $hash : 'orderdetail');
return Context::getContext()->link->getPageLink('get-file&key=' . $key);
}
/**
* Return html link.
*
* @param string|bool $class CSS selector
* @param string|bool $hash hash code in table order detail
*
* @return string Html all the code for print a link to the file
*/
public function getHtmlLink($class = false, $hash = false)
{
$link = $this->getTextLink($hash);
$html = '<a href="' . $link . '" title=""';
if ($class) {
$html .= ' class="' . $class . '"';
}
$html .= '>' . $this->display_filename . '</a>';
return $html;
}
/**
* Return a deadline.
*
* @return string Datetime in SQL format
*/
public function getDeadline()
{
if (!(int) $this->nb_days_accessible) {
return '0000-00-00 00:00:00';
}
$timestamp = strtotime('+' . (int) $this->nb_days_accessible . ' day');
return date('Y-m-d H:i:s', $timestamp);
}
/**
* Return a hash for control download access.
*
* @return string Hash ready to insert in database
*/
public function getHash()
{
// TODO check if this hash not already in database
return sha1(microtime() . $this->id);
}
/**
* Return a sha1 filename.
*
* @return string Sha1 unique filename
*/
public static function getNewFilename()
{
do {
$filename = sha1(microtime());
} while (file_exists(_PS_DOWNLOAD_DIR_ . $filename));
return $filename;
}
/**
* This method is allow to know if a feature is used or active.
*
* @return bool
*/
public static function isFeatureActive()
{
return Configuration::get('PS_VIRTUAL_PROD_FEATURE_ACTIVE');
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShop\PrestaShop\Adapter\Configuration as AdapterConfiguration;
use PrestaShop\PrestaShop\Adapter\HookManager;
use PrestaShop\PrestaShop\Adapter\Image\ImageRetriever;
use PrestaShop\PrestaShop\Adapter\Presenter\Product\ProductListingPresenter;
use PrestaShop\PrestaShop\Adapter\Presenter\Product\ProductPresenter;
use PrestaShop\PrestaShop\Adapter\Product\PriceFormatter;
use PrestaShop\PrestaShop\Adapter\Product\ProductColorsRetriever;
use PrestaShop\PrestaShop\Core\Product\ProductPresentationSettings;
/**
* Class ProductPresenterFactoryCore.
*/
class ProductPresenterFactoryCore
{
private $context;
private $taxConfiguration;
/**
* ProductPresenterFactoryCore constructor.
*
* @param Context $context
* @param TaxConfiguration|null $taxConfiguration
*/
public function __construct(Context $context, ?TaxConfiguration $taxConfiguration = null)
{
$this->context = $context;
$this->taxConfiguration = (null === $taxConfiguration) ? new TaxConfiguration() : $taxConfiguration;
}
/**
* Get presentation settings.
*
* @return ProductPresentationSettings
*/
public function getPresentationSettings()
{
$settings = new ProductPresentationSettings();
$settings->catalog_mode = Configuration::isCatalogMode();
$settings->catalog_mode_with_prices = (int) Configuration::get('PS_CATALOG_MODE_WITH_PRICES');
$settings->include_taxes = $this->taxConfiguration->includeTaxes();
$settings->allow_add_variant_to_cart_from_listing = (int) Configuration::get('PS_ATTRIBUTE_CATEGORY_DISPLAY');
$settings->stock_management_enabled = Configuration::get('PS_STOCK_MANAGEMENT');
$settings->showPrices = Configuration::showPrices();
$settings->lastRemainingItems = Configuration::get('PS_LAST_QTIES');
$settings->showLabelOOSListingPages = (bool) Configuration::get('PS_SHOW_LABEL_OOS_LISTING_PAGES');
return $settings;
}
/**
* Get presenter.
*
* @return ProductListingPresenter|ProductPresenter
*/
public function getPresenter()
{
$imageRetriever = new ImageRetriever(
$this->context->link
);
if (is_a($this->context->controller, 'ProductListingFrontControllerCore')) {
return new ProductListingPresenter(
$imageRetriever,
$this->context->link,
new PriceFormatter(),
new ProductColorsRetriever(),
$this->context->getTranslator()
);
}
return new ProductPresenter(
$imageRetriever,
$this->context->link,
new PriceFormatter(),
new ProductColorsRetriever(),
$this->context->getTranslator(),
new HookManager(),
new AdapterConfiguration()
);
}
}

270
classes/ProductSale.php Normal file
View File

@@ -0,0 +1,270 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class ProductSaleCore.
*/
class ProductSaleCore
{
/**
* Fill the `product_sale` SQL table with data from `order_detail`.
*
* @return bool True on success
*/
public static function fillProductSales()
{
$sql = 'REPLACE INTO ' . _DB_PREFIX_ . 'product_sale
(`id_product`, `quantity`, `sale_nbr`, `date_upd`)
SELECT od.product_id, SUM(od.product_quantity), COUNT(od.product_id), NOW()
FROM ' . _DB_PREFIX_ . 'order_detail od GROUP BY od.product_id';
return Db::getInstance()->execute($sql);
}
/**
* Get number of actives products sold.
*
* @return int number of actives products listed in product_sales
*/
public static function getNbSales()
{
$sql = 'SELECT COUNT(ps.`id_product`) AS nb
FROM `' . _DB_PREFIX_ . 'product_sale` ps
LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON p.`id_product` = ps.`id_product`
' . Shop::addSqlAssociation('product', 'p', false) . '
WHERE product_shop.`active` = 1';
return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);
}
/**
* Get required informations on best sales products.
*
* @param int $idLang Language id
* @param int $pageNumber Start from (optional)
* @param int $nbProducts Number of products to return (optional)
*
* @return array|bool
*/
public static function getBestSales($idLang, $pageNumber = 0, $nbProducts = 10, $orderBy = null, $orderWay = null)
{
$context = Context::getContext();
if ($pageNumber < 1) {
$pageNumber = 1;
}
if ($nbProducts < 1) {
$nbProducts = 10;
}
$finalOrderBy = $orderBy;
$orderTable = '';
$invalidOrderBy = !Validate::isOrderBy($orderBy);
if ($invalidOrderBy || null === $orderBy) {
$orderBy = 'quantity';
$orderTable = 'ps';
}
if ($orderBy == 'date_add' || $orderBy == 'date_upd') {
$orderTable = 'product_shop';
}
$invalidOrderWay = !Validate::isOrderWay($orderWay);
if ($invalidOrderWay || null === $orderWay || $orderBy == 'sales') {
$orderWay = 'DESC';
}
$interval = Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20;
// no group by needed : there's only one attribute with default_on=1 for a given id_product + shop
// same for image with cover=1
$sql = 'SELECT p.*, product_shop.*, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity,
' . (Combination::isFeatureActive() ? 'product_attribute_shop.minimal_quantity AS product_attribute_minimal_quantity,IFNULL(product_attribute_shop.id_product_attribute,0) id_product_attribute,' : '') . '
pl.`description`, pl.`description_short`, pl.`link_rewrite`, pl.`meta_description`,
pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`,
m.`name` AS manufacturer_name, p.`id_manufacturer` as id_manufacturer,
image_shop.`id_image` id_image, il.`legend`,
ps.`quantity` AS sales, t.`rate`, pl.`meta_title`, pl.`meta_description`,
DATEDIFF(p.`date_add`, DATE_SUB("' . date('Y-m-d') . ' 00:00:00",
INTERVAL ' . (int) $interval . ' DAY)) > 0 AS new'
. ' FROM `' . _DB_PREFIX_ . 'product_sale` ps
LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON ps.`id_product` = p.`id_product`
' . Shop::addSqlAssociation('product', 'p', false);
if (Combination::isFeatureActive()) {
$sql .= ' LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` product_attribute_shop
ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shop.id_shop=' . (int) $context->shop->id . ')';
}
$sql .= ' LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl
ON p.`id_product` = pl.`id_product`
AND pl.`id_lang` = ' . (int) $idLang . Shop::addSqlRestrictionOnLang('pl') . '
LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
ON (image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop=' . (int) $context->shop->id . ')
LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $idLang . ')
LEFT JOIN `' . _DB_PREFIX_ . 'manufacturer` m ON (m.`id_manufacturer` = p.`id_manufacturer`)
LEFT JOIN `' . _DB_PREFIX_ . 'tax_rule` tr ON (product_shop.`id_tax_rules_group` = tr.`id_tax_rules_group`)
AND tr.`id_country` = ' . (int) $context->country->id . '
AND tr.`id_state` = 0
LEFT JOIN `' . _DB_PREFIX_ . 'tax` t ON (t.`id_tax` = tr.`id_tax`)
' . Product::sqlStock('p', 0);
$sql .= '
WHERE product_shop.`active` = 1
AND product_shop.`visibility` != \'none\'';
if (Group::isFeatureActive()) {
$groups = FrontController::getCurrentCustomerGroups();
$sql .= ' AND EXISTS(SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp
JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cp.id_category = cg.id_category AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id) . ')
WHERE cp.`id_product` = p.`id_product`)';
}
if ($finalOrderBy != 'price') {
$sql .= '
ORDER BY ' . (!empty($orderTable) ? '`' . pSQL($orderTable) . '`.' : '') . '`' . pSQL($orderBy) . '` ' . pSQL($orderWay) . '
LIMIT ' . (int) (($pageNumber - 1) * $nbProducts) . ', ' . (int) $nbProducts;
}
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
if ($finalOrderBy == 'price') {
Tools::orderbyPrice($result, $orderWay);
$result = array_slice($result, (int) (($pageNumber - 1) * $nbProducts), (int) $nbProducts);
}
if (!$result) {
return false;
}
return $result;
}
/**
* Get required informations on best sales products.
*
* @param int $idLang Language id
* @param int $pageNumber Start from (optional)
* @param int $nbProducts Number of products to return (optional)
*
* @return bool|array keys : id_product, link_rewrite, name, id_image, legend, sales, ean13, upc, link
*/
public static function getBestSalesLight($idLang, $pageNumber = 0, $nbProducts = 10, ?Context $context = null)
{
if (!$context) {
$context = Context::getContext();
}
if ($pageNumber < 0) {
$pageNumber = 0;
}
if ($nbProducts < 1) {
$nbProducts = 10;
}
// no group by needed : there's only one attribute with default_on=1 for a given id_product + shop
// same for image with cover=1
$sql = '
SELECT
p.id_product, IFNULL(product_attribute_shop.id_product_attribute,0) id_product_attribute, pl.`link_rewrite`, pl.`name`, pl.`description_short`, product_shop.`id_category_default`,
image_shop.`id_image` id_image, il.`legend`,
ps.`quantity` AS sales, p.`ean13`, p.`upc`, cl.`link_rewrite` AS category, p.show_price, p.available_for_order, IFNULL(stock.quantity, 0) as quantity, p.customizable,
IFNULL(pa.minimal_quantity, p.minimal_quantity) as minimal_quantity, stock.out_of_stock,
product_shop.`date_add` > "' . date('Y-m-d', strtotime('-' . (Configuration::get('PS_NB_DAYS_NEW_PRODUCT') ? (int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20) . ' DAY')) . '" as new,
product_shop.`on_sale`, product_attribute_shop.minimal_quantity AS product_attribute_minimal_quantity
FROM `' . _DB_PREFIX_ . 'product_sale` ps
LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON ps.`id_product` = p.`id_product`
' . Shop::addSqlAssociation('product', 'p') . '
LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` product_attribute_shop
ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shop.id_shop=' . (int) $context->shop->id . ')
LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON (product_attribute_shop.id_product_attribute=pa.id_product_attribute)
LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl
ON p.`id_product` = pl.`id_product`
AND pl.`id_lang` = ' . (int) $idLang . Shop::addSqlRestrictionOnLang('pl') . '
LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
ON (image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop=' . (int) $context->shop->id . ')
LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $idLang . ')
LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl
ON cl.`id_category` = product_shop.`id_category_default`
AND cl.`id_lang` = ' . (int) $idLang . Shop::addSqlRestrictionOnLang('cl') . Product::sqlStock('p', 0);
$sql .= '
WHERE product_shop.`active` = 1
AND p.`visibility` != \'none\'';
if (Group::isFeatureActive()) {
$groups = FrontController::getCurrentCustomerGroups();
$sql .= ' AND EXISTS(SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp
JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cp.id_category = cg.id_category AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id) . ')
WHERE cp.`id_product` = p.`id_product`)';
}
$sql .= '
ORDER BY ps.quantity DESC
LIMIT ' . (int) ($pageNumber * $nbProducts) . ', ' . (int) $nbProducts;
if (!$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql)) {
return false;
}
return $result;
}
/**
* Add Product sale.
*
* @param int $productId Product ID
* @param int $qty Quantity
*
* @return bool Indicates whether the sale was successfully added
*/
public static function addProductSale($productId, $qty = 1)
{
return Db::getInstance()->execute('
INSERT INTO ' . _DB_PREFIX_ . 'product_sale
(`id_product`, `quantity`, `sale_nbr`, `date_upd`)
VALUES (' . (int) $productId . ', ' . (int) $qty . ', 1, NOW())
ON DUPLICATE KEY UPDATE `quantity` = `quantity` + ' . (int) $qty . ', `sale_nbr` = `sale_nbr` + 1, `date_upd` = NOW()');
}
/**
* Get number of sales.
*
* @param int $idProduct Product ID
*
* @return int Number of sales for the given Product
*/
public static function getNbrSales($idProduct)
{
$result = Db::getInstance()->getRow('SELECT `sale_nbr` FROM ' . _DB_PREFIX_ . 'product_sale WHERE `id_product` = ' . (int) $idProduct);
if (empty($result) || !array_key_exists('sale_nbr', $result)) {
return -1;
}
return (int) $result['sale_nbr'];
}
/**
* Remove a Product sale.
*
* @param int $idProduct Product ID
* @param int $qty Quantity
*
* @return bool Indicates whether the product sale has been successfully removed
*/
public static function removeProductSale($idProduct, $qty = 1)
{
$totalSales = ProductSale::getNbrSales($idProduct);
if ($totalSales > 1) {
return Db::getInstance()->execute(
'
UPDATE ' . _DB_PREFIX_ . 'product_sale
SET `quantity` = CAST(`quantity` AS SIGNED) - ' . (int) $qty . ', `sale_nbr` = CAST(`sale_nbr` AS SIGNED) - 1, `date_upd` = NOW()
WHERE `id_product` = ' . (int) $idProduct
);
} elseif ($totalSales == 1) {
return Db::getInstance()->delete('product_sale', 'id_product = ' . (int) $idProduct);
}
return true;
}
}

238
classes/ProductSupplier.php Normal file
View File

@@ -0,0 +1,238 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* ProductSupplierCore class.
*/
class ProductSupplierCore extends ObjectModel
{
/**
* @var int product ID
* */
public $id_product;
/**
* @var int product attribute ID
* */
public $id_product_attribute;
/**
* @var int the supplier ID
* */
public $id_supplier;
/**
* @var string The supplier reference of the product
* */
public $product_supplier_reference;
/**
* @var int the currency ID for unit price tax excluded
* */
public $id_currency;
/**
* @var float The unit price tax excluded of the product
* */
public $product_supplier_price_te;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'product_supplier',
'primary' => 'id_product_supplier',
'fields' => [
'product_supplier_reference' => ['type' => self::TYPE_STRING, 'validate' => 'isReference', 'size' => 64],
'id_product' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_product_attribute' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_supplier' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'product_supplier_price_te' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
'id_currency' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
],
];
/**
* @see ObjectModel::$webserviceParameters
*/
protected $webserviceParameters = [
'objectsNodeName' => 'product_suppliers',
'objectNodeName' => 'product_supplier',
'fields' => [
'id_product' => ['xlink_resource' => 'products'],
'id_product_attribute' => ['xlink_resource' => 'combinations'],
'id_supplier' => ['xlink_resource' => 'suppliers'],
'id_currency' => ['xlink_resource' => 'currencies'],
],
];
/**
* For a given product and supplier, gets the product supplier reference.
*
* @param int $idProduct Product ID
* @param int $idProductAttribute Product Attribute ID
* @param int $idSupplier Supplier ID
*
* @return string|false Product Supplier reference
*/
public static function getProductSupplierReference($idProduct, $idProductAttribute, $idSupplier)
{
// build query
$query = new DbQuery();
$query->select('ps.product_supplier_reference');
$query->from('product_supplier', 'ps');
$query->where(
'ps.id_product = ' . (int) $idProduct . '
AND ps.id_product_attribute = ' . (int) $idProductAttribute . '
AND ps.id_supplier = ' . (int) $idSupplier
);
return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query);
}
/**
* For a given product and supplier, gets the product supplier unit price.
*
* @param int $idProduct Product ID
* @param int $idProductAttribute Product Attribute ID
* @param int $idSupplier Supplier ID
* @param bool $withCurrency Optional With currency
*
* @return string|array
*/
public static function getProductSupplierPrice($idProduct, $idProductAttribute, $idSupplier, $withCurrency = false)
{
// build query
$query = new DbQuery();
$query->select('ps.product_supplier_price_te');
if ($withCurrency) {
$query->select('ps.id_currency');
}
$query->from('product_supplier', 'ps');
$query->where(
'ps.id_product = ' . (int) $idProduct . '
AND ps.id_product_attribute = ' . (int) $idProductAttribute . '
AND ps.id_supplier = ' . (int) $idSupplier
);
if (!$withCurrency) {
return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query);
}
$res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query);
if (isset($res[0])) {
return $res[0];
}
return $res;
}
/**
* For a given product and supplier, gets corresponding ProductSupplier ID.
*
* @param int $idProduct
* @param int $idProductAttribute
* @param int $idSupplier
*
* @return int
*/
public static function getIdByProductAndSupplier($idProduct, $idProductAttribute, $idSupplier)
{
$query = new DbQuery();
$query->select('ps.id_product_supplier');
$query->from('product_supplier', 'ps');
$query->where(
'ps.id_product = ' . (int) $idProduct . '
AND ps.id_product_attribute = ' . (int) $idProductAttribute . '
AND ps.id_supplier = ' . (int) $idSupplier
);
return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query);
}
/**
* For a given product, retrieves its suppliers.
*
* @param int $idProduct
* @param bool $groupBySupplier
*
* @return PrestaShopCollection Collection of ProductSupplier
*/
public static function getSupplierCollection($idProduct, $groupBySupplier = true)
{
$suppliers = new PrestaShopCollection('ProductSupplier');
$suppliers->where('id_product', '=', (int) $idProduct);
if ($groupBySupplier) {
$suppliers->groupBy('id_supplier');
}
return $suppliers;
}
/**
* For a given Supplier, Product, returns the purchased price.
*
* @param int|null $idSupplier
* @param int|null $idProduct
* @param int $idProductAttribute Optional
* @param bool $convertedPrice Optional
*
* @return float|null
*/
public static function getProductPrice($idSupplier, $idProduct, $idProductAttribute = 0, $convertedPrice = false)
{
if (null === $idSupplier || null === $idProduct) {
return null;
}
$query = new DbQuery();
$query->select('product_supplier_price_te as price_te, id_currency');
$query->from('product_supplier');
$query->where('id_product = ' . (int) $idProduct . ' AND id_product_attribute = ' . (int) $idProductAttribute);
$query->where('id_supplier = ' . (int) $idSupplier);
$row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($query);
if (empty($row)) {
return null;
}
if ($convertedPrice) {
return Tools::convertPrice($row['price_te'], $row['id_currency']);
}
return $row['price_te'];
}
/**
* For a given product and supplier, gets the product supplier datas.
*
* @param int $idProduct Product ID
* @param int $idProductAttribute Product Attribute ID
* @param int $idSupplier Supplier ID
*
* @return array
*/
public static function getProductSupplierData($idProduct, $idProductAttribute, $idSupplier)
{
// build query
$query = new DbQuery();
$query->select('ps.product_supplier_reference, ps.product_supplier_price_te as price, ps.id_currency');
$query->from('product_supplier', 'ps');
$query->where(
'ps.id_product = ' . (int) $idProduct . '
AND ps.id_product_attribute = ' . (int) $idProductAttribute . '
AND ps.id_supplier = ' . (int) $idSupplier
);
$res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query);
if (isset($res[0])) {
return $res[0];
}
return $res;
}
}

250
classes/Profile.php Normal file
View File

@@ -0,0 +1,250 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class ProfileCore.
*/
class ProfileCore extends ObjectModel
{
public const ALLOWED_PROFILE_TYPE_CHECK = [
'id_tab',
'class_name',
];
/** @var string|array<int, string> Name */
public $name;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'profile',
'primary' => 'id_profile',
'multilang' => true,
'fields' => [
/* Lang fields */
'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 128],
],
];
protected static $_cache_accesses = [];
/**
* {@inheritdoc}
*/
public function __construct($id = null, $idLang = null, $idShop = null, $translator = null)
{
parent::__construct($id, $idLang, $idShop, $translator);
$this->image_dir = _PS_PROFILE_IMG_DIR_;
}
/**
* @return string|null
*/
public function getProfileImage(): ?string
{
$path = $this->image_dir . $this->id . '.jpg';
return file_exists($path)
? Context::getContext()->link->getMediaLink(
str_replace($this->image_dir, _THEME_PROFILE_DIR_, $path)
)
: null;
}
/**
* Get all available profiles.
*
* @return array Profiles
*/
public static function getProfiles($idLang)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT p.`id_profile`, `name`
FROM `' . _DB_PREFIX_ . 'profile` p
LEFT JOIN `' . _DB_PREFIX_ . 'profile_lang` pl ON (p.`id_profile` = pl.`id_profile` AND `id_lang` = ' . (int) $idLang . ')
ORDER BY `id_profile` ASC');
}
/**
* Get the current profile name.
*
* @param int $idProfile Profile ID
* @param int|null $idLang Language ID
*
* @return array Profile
*/
public static function getProfile($idProfile, $idLang = null)
{
if (!$idLang) {
$idLang = Configuration::get('PS_LANG_DEFAULT');
}
return Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow(
'SELECT `name`
FROM `' . _DB_PREFIX_ . 'profile` p
LEFT JOIN `' . _DB_PREFIX_ . 'profile_lang` pl ON (p.`id_profile` = pl.`id_profile`)
WHERE p.`id_profile` = ' . (int) $idProfile . '
AND pl.`id_lang` = ' . (int) $idLang
);
}
public function add($autodate = true, $null_values = false)
{
return parent::add($autodate, true);
}
public function delete()
{
if (parent::delete()) {
return
Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'access` WHERE `id_profile` = ' . (int) $this->id)
&& Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'module_access` WHERE `id_profile` = ' . (int) $this->id);
}
return false;
}
/**
* Get access profile.
*
* @param int $idProfile Profile ID
* @param int $idTab Tab ID
*
* @return array|bool
*/
public static function getProfileAccess($idProfile, $idTab)
{
// getProfileAccesses is cached so there is no performance leak
$accesses = Profile::getProfileAccesses($idProfile);
return isset($accesses[$idTab]) ? $accesses[$idTab] : false;
}
/**
* Get access profiles.
*
* @param int $idProfile Profile ID
* @param string $type Type
*
* @return array|false
*/
public static function getProfileAccesses($idProfile, $type = 'id_tab')
{
if (!in_array($type, self::ALLOWED_PROFILE_TYPE_CHECK)) {
return false;
}
if (!isset(self::$_cache_accesses[$idProfile])) {
self::$_cache_accesses[$idProfile] = [];
}
if (!isset(self::$_cache_accesses[$idProfile][$type])) {
self::$_cache_accesses[$idProfile][$type] = [];
// Super admin profile has full auth
if ($idProfile == _PS_ADMIN_PROFILE_) {
$defaultPermission = [
'id_profile' => _PS_ADMIN_PROFILE_,
'view' => '1',
'add' => '1',
'edit' => '1',
'delete' => '1',
];
$roles = [];
} else {
$defaultPermission = [
'id_profile' => $idProfile,
'view' => '0',
'add' => '0',
'edit' => '0',
'delete' => '0',
];
$roles = self::generateAccessesArrayFromPermissions(
Db::getInstance()->executeS('
SELECT `slug`,
`slug` LIKE "%CREATE" as "add",
`slug` LIKE "%READ" as "view",
`slug` LIKE "%UPDATE" as "edit",
`slug` LIKE "%DELETE" as "delete"
FROM `' . _DB_PREFIX_ . 'authorization_role` a
LEFT JOIN `' . _DB_PREFIX_ . 'access` j ON j.id_authorization_role = a.id_authorization_role
WHERE j.`id_profile` = ' . (int) $idProfile)
);
}
self::fillCacheAccesses(
$idProfile,
$defaultPermission,
$roles
);
}
return self::$_cache_accesses[$idProfile][$type];
}
public static function resetStaticCache()
{
parent::resetStaticCache();
self::resetCacheAccesses();
}
public static function resetCacheAccesses()
{
self::$_cache_accesses = [];
}
/**
* @param int $idProfile Profile ID
* @param array $defaultData Cached data
* @param array $accesses Data loaded from the database
*/
private static function fillCacheAccesses($idProfile, $defaultData = [], $accesses = [])
{
foreach (Tab::getTabs(Context::getContext()->language->id) as $tab) {
$accessData = [];
if (isset($accesses[strtoupper($tab['class_name'])])) {
$accessData = $accesses[strtoupper($tab['class_name'])];
}
foreach (self::ALLOWED_PROFILE_TYPE_CHECK as $type) {
self::$_cache_accesses[$idProfile][$type][$tab[$type]] = array_merge(
[
'id_tab' => $tab['id_tab'],
'class_name' => $tab['class_name'],
],
$defaultData,
$accessData
);
}
}
}
/**
* Creates the array of accesses [role => add / view / edit / delete] from a given list of roles
*
* @param array $rolesGiven
*
* @return array
*/
private static function generateAccessesArrayFromPermissions($rolesGiven)
{
// Modify array to merge the class names together.
$accessPerTab = [];
foreach ($rolesGiven as $role) {
preg_match(
'/ROLE_MOD_[A-Z]+_(?P<classname>[A-Z][A-Z0-9]*)_[A-Z]+/',
$role['slug'],
$matches
);
if (empty($matches['classname'])) {
continue;
}
$accessPerTab[$matches['classname']][array_search('1', $role)] = '1';
}
return $accessPerTab;
}
}

93
classes/QuickAccess.php Normal file
View File

@@ -0,0 +1,93 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class QuickAccessCore.
*/
class QuickAccessCore extends ObjectModel
{
/** @var string Name */
public $name;
/** @var string Link */
public $link;
/** @var bool New windows or not */
public $new_window;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'quick_access',
'primary' => 'id_quick_access',
'multilang' => true,
'fields' => [
'link' => ['type' => self::TYPE_STRING, 'validate' => 'isUrl', 'required' => true, 'size' => 255],
'new_window' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'required' => true],
/* Lang fields */
'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isCleanHtml', 'required' => true, 'size' => 32],
],
];
/**
* Get all available quick_accesses.
*
* @return array QuickAccesses
*/
public static function getQuickAccesses($idLang)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT *
FROM `' . _DB_PREFIX_ . 'quick_access` qa
LEFT JOIN `' . _DB_PREFIX_ . 'quick_access_lang` qal ON (qa.`id_quick_access` = qal.`id_quick_access` AND qal.`id_lang` = ' . (int) $idLang . ')
ORDER BY `name` ASC');
}
/**
* Get all available quick_accesses with token.
*
* @return bool|array QuickAccesses
*/
public static function getQuickAccessesWithToken($idLang, $idEmployee)
{
$quickAccess = self::getQuickAccesses($idLang);
if (empty($quickAccess)) {
return false;
}
$container = PrestaShop\PrestaShop\Adapter\SymfonyContainer::getInstance();
if (!$container) {
return false;
}
$quickAccessGenerator = $container->get(PrestaShop\PrestaShop\Core\QuickAccess\QuickAccessGenerator::class);
return $quickAccessGenerator->getTokenizedQuickAccesses();
}
/**
* Toggle new window.
*
* @return bool
*
* @throws PrestaShopException
*/
public function toggleNewWindow()
{
if (!array_key_exists('new_window', get_object_vars($this))) {
throw new PrestaShopException('property "new_window" is missing in object ' . get_class($this));
}
$this->setFieldsToUpdate(['new_window' => true]);
$this->new_window = !(int) $this->new_window;
return $this->update(false);
}
}

658
classes/RequestSql.php Normal file
View File

@@ -0,0 +1,658 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShopBundle\Form\Admin\Type\FormattedTextareaType;
/**
* Class RequestSqlCore.
*/
class RequestSqlCore extends ObjectModel
{
public $name;
public $sql;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'request_sql',
'primary' => 'id_request_sql',
'fields' => [
'name' => ['type' => self::TYPE_STRING, 'validate' => 'isString', 'required' => true, 'size' => 200],
'sql' => ['type' => self::TYPE_SQL, 'validate' => 'isString', 'required' => true, 'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4],
],
];
/** @var array : List of params to tested */
public $tested = [
'required' => ['SELECT', 'FROM'],
'option' => ['WHERE', 'ORDER', 'LIMIT', 'HAVING', 'GROUP', 'UNION'],
'operator' => [
'AND', '&&', 'BETWEEN', 'AND', 'BINARY', '&', '~', '|', '^', 'CASE', 'WHEN', 'END', 'DIV', '/', '<=>', '=', '>=',
'>', 'IS', 'NOT', 'NULL', '<<', '<=', '<', 'LIKE', '-', '%', '!=', '<>', 'REGEXP', '!', '||', 'OR', '+', '>>', 'RLIKE', 'SOUNDS', '*',
'-', 'XOR', 'IN',
],
'function' => [
'AVG', 'SUM', 'COUNT', 'MIN', 'MAX', 'STDDEV', 'STDDEV_SAMP', 'STDDEV_POP', 'VARIANCE', 'VAR_SAMP', 'VAR_POP',
'GROUP_CONCAT', 'BIT_AND', 'BIT_OR', 'BIT_XOR',
],
'unauthorized' => [
'DELETE', 'ALTER', 'INSERT', 'REPLACE', 'CREATE', 'TRUNCATE', 'OPTIMIZE', 'GRANT', 'REVOKE', 'SHOW', 'HANDLER',
'LOAD', 'LOAD_FILE', 'ROLLBACK', 'SAVEPOINT', 'UNLOCK', 'INSTALL', 'UNINSTALL', 'ANALZYE', 'BACKUP', 'CHECK', 'CHECKSUM', 'REPAIR', 'RESTORE', 'CACHE',
'DESCRIBE', 'EXPLAIN', 'USE', 'HELP', 'SET', 'DUPLICATE', 'VALUES', 'INTO', 'RENAME', 'CALL', 'PROCEDURE', 'FUNCTION', 'DATABASE', 'SERVER',
'LOGFILE', 'DEFINER', 'RETURNS', 'EVENT', 'TABLESPACE', 'VIEW', 'TRIGGER', 'DATA', 'DO', 'PASSWORD', 'USER', 'PLUGIN', 'FLUSH', 'KILL',
'RESET', 'START', 'STOP', 'PURGE', 'EXECUTE', 'PREPARE', 'DEALLOCATE', 'LOCK', 'USING', 'DROP', 'FOR', 'UPDATE', 'BEGIN', 'BY', 'ALL', 'SHARE',
'MODE', 'TO', 'KEY', 'DISTINCTROW', 'DISTINCT', 'HIGH_PRIORITY', 'LOW_PRIORITY', 'DELAYED', 'IGNORE', 'FORCE', 'STRAIGHT_JOIN',
'SQL_SMALL_RESULT', 'SQL_BIG_RESULT', 'QUICK', 'SQL_BUFFER_RESULT', 'SQL_CACHE', 'SQL_NO_CACHE', 'SQL_CALC_FOUND_ROWS', 'WITH',
'OUTFILE', 'DUMPFILE',
],
];
public $attributes = [
'passwd' => '*******************',
'secure_key' => '*******************',
];
/** @var array : list of errors */
public $error_sql = [];
/**
* Get list of request SQL.
*
* @return array|bool
*/
public static function getRequestSql()
{
if (!$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT * FROM `' . _DB_PREFIX_ . 'request_sql` ORDER BY `id_request_sql`')) {
return false;
}
$requestSql = [];
foreach ($result as $row) {
$requestSql[] = $row['sql'];
}
return $requestSql;
}
/**
* Get list of request SQL by id request.
*
* @param int $id
*
* @return array
*/
public static function getRequestSqlById($id)
{
return Db::getInstance()->executeS('SELECT `sql` FROM `' . _DB_PREFIX_ . 'request_sql` WHERE `id_request_sql` = ' . (int) $id);
}
/**
* Call the parserSQL() method in Tools class
* Cut the request in table for check it.
*
* @param string $sql
*
* @return array|bool
*/
public function parsingSql($sql)
{
return Tools::parserSQL($sql);
}
/**
* Check if the parsing of the SQL request is good or not.
*
* @param array $tab
* @param bool $in
* @param string $sql
*
* @return bool
*/
public function validateParser($tab, $in, $sql)
{
if (!$tab) {
return false;
} elseif (isset($tab['UNION'])) {
$union = $tab['UNION'];
foreach ($union as $tab) {
if (!$this->validateSql($tab, $in, $sql)) {
return false;
}
}
return true;
} else {
return $this->validateSql($tab, $in, $sql);
}
}
/**
* Cut the request for check each cutting.
*
* @param array<string, array> $tab
* @param bool $in
* @param string $sql
*
* @return bool
*/
public function validateSql($tab, $in, $sql)
{
if (!$this->testedRequired($tab)) {
return false;
} elseif (!$this->testedUnauthorized($tab)) {
return false;
} elseif (!$this->checkedFrom($tab['FROM'])) {
return false;
} elseif (!$this->checkedSelect($tab['SELECT'], $tab['FROM'], $in)) {
return false;
} elseif (isset($tab['WHERE'])) {
if (!$this->checkedWhere($tab['WHERE'], $tab['FROM'], $sql)) {
return false;
}
} elseif (isset($tab['HAVING'])) {
if (!$this->checkedHaving($tab['HAVING'], $tab['FROM'])) {
return false;
}
} elseif (isset($tab['ORDER'])) {
if (!$this->checkedOrder($tab['ORDER'], $tab['FROM'])) {
return false;
}
} elseif (isset($tab['GROUP'])) {
if (!$this->checkedGroupBy($tab['GROUP'], $tab['FROM'])) {
return false;
}
} elseif (isset($tab['LIMIT'])) {
if (!$this->checkedLimit($tab['LIMIT'])) {
return false;
}
}
if (empty($this->_errors) && !Db::getInstance()->executeS($sql)) {
return false;
}
return true;
}
/**
* Get list of all tables.
*
* @return array
*/
public function getTables()
{
$results = Db::getInstance()->executeS('SHOW TABLES');
$tables = [];
foreach ($results as $result) {
$key = array_keys($result);
$tables[] = $result[$key[0]];
}
return $tables;
}
/**
* Get list of all attributes by an table.
*
* @param string $table
*
* @return array
*/
public function getAttributesByTable($table)
{
return Db::getInstance()->executeS('DESCRIBE ' . pSQL($table));
}
/**
* Cut an join sentence.
*
* @param array $attrs
* @param array $from
*
* @return array
*/
public function cutJoin($attrs, $from)
{
$tab = [];
foreach ($attrs as $attr) {
if (in_array($attr['expr_type'], ['operator', 'const'])) {
continue;
}
if (!empty($attr['sub_tree'])) {
foreach ($attr['sub_tree'] as $treeItem) {
if ($treeItem['expr_type'] !== 'colref') {
continue;
}
if ($attribut = $this->cutAttribute($treeItem['base_expr'], $from)) {
$tab[] = $attribut;
}
}
} else {
if ($attribut = $this->cutAttribute($attr['base_expr'], $from)) {
$tab[] = $attribut;
}
}
}
return $tab;
}
/**
* Cut an attribute with or without the alias.
*
* @param string $attr
* @param array $from
*
* @return array|bool
*/
public function cutAttribute($attr, $from)
{
$matches = [];
if (preg_match('/((`(\()?([a-z0-9_])+`(\))?)|((\()?([a-z0-9_])+(\))?))\.((`(\()?([a-z0-9_])+`(\))?)|((\()?([a-z0-9_])+(\))?))$/i', $attr, $matches, PREG_OFFSET_CAPTURE)) {
$tab = explode('.', str_replace(['`', '(', ')'], '', $matches[0][0]));
if ($table = $this->returnNameTable($tab[0], $from)) {
return [
'table' => $table,
'alias' => $tab[0],
'attribut' => $tab[1],
'string' => $attr,
];
}
} elseif (preg_match('/((`(\()?([a-z0-9_])+`(\))?)|((\()?([a-z0-9_])+(\))?))$/i', $attr, $matches, PREG_OFFSET_CAPTURE)) {
$attribut = str_replace(['`', '(', ')'], '', $matches[0][0]);
if ($table = $this->returnNameTable(false, $from, $attr)) {
return [
'table' => $table,
'attribut' => $attribut,
'string' => $attr,
];
}
}
return false;
}
/**
* Get name of table by alias.
*
* @param string|false $alias
* @param array $tables
*
* @return array|bool
*/
public function returnNameTable($alias, $tables, $attr = null)
{
if ($alias) {
foreach ($tables as $table) {
if (!isset($table['alias']) || !isset($table['table'])) {
continue;
}
/** @var string|array{'parts': array<int, bool>} $tableAlias */
$tableAlias = $table['alias']['no_quotes'];
if ($tableAlias == $alias || $tableAlias['parts'][0] == $alias) {
return [$table['table']];
}
}
} elseif (count($tables) > 1) {
if ($attr !== null) {
$tab = [];
foreach ($tables as $table) {
if ($this->attributExistInTable($attr, $table['table'])) {
$tab[] = $table['table'];
}
}
if (count($tab) == 1) {
return $tab;
}
}
$this->error_sql['returnNameTable'] = false;
return false;
}
$tab = [];
foreach ($tables as $table) {
$tab[] = $table['table'];
}
return $tab;
}
/**
* Check if an attributes exists in a table.
*
* @param string $attr
* @param array $table
*
* @return bool
*/
public function attributExistInTable($attr, $table)
{
if (!$attr) {
return true;
}
if (is_array($table) && (count($table) == 1)) {
$table = $table[0];
}
$attributs = $this->getAttributesByTable($table);
foreach ($attributs as $attribut) {
if ($attribut['Field'] == trim($attr, ' `')) {
return true;
}
}
return false;
}
/**
* Check if all required sentence existing.
*
* @param array $tab
*
* @return bool
*/
public function testedRequired($tab)
{
foreach ($this->tested['required'] as $key) {
if (!array_key_exists($key, $tab)) {
$this->error_sql['testedRequired'] = $key;
return false;
}
}
return true;
}
/**
* Check if an unauthorized existing in an array.
*
* @param array $tab
*
* @return bool
*/
public function testedUnauthorized($tab)
{
foreach ($this->tested['unauthorized'] as $key) {
if (array_key_exists($key, $tab)) {
$this->error_sql['testedUnauthorized'] = $key;
return false;
}
}
return true;
}
/**
* Check a "FROM" sentence.
*
* @param array<int, array<string, mixed>> $from
*
* @return bool
*/
public function checkedFrom($from)
{
$nb = count($from);
for ($i = 0; $i < $nb; ++$i) {
$table = $from[$i];
if (isset($table['table']) && !in_array(str_replace('`', '', $table['table']), $this->getTables())) {
$this->error_sql['checkedFrom']['table'] = $table['table'];
return false;
}
if ($table['ref_type'] == 'ON' && (trim($table['join_type']) == 'LEFT' || trim($table['join_type']) == 'JOIN')) {
$attrs = $this->cutJoin($table['ref_clause'], $from);
foreach ($attrs as $attr) {
if (!$this->attributExistInTable($attr['attribut'], $attr['table'])) {
$this->error_sql['checkedFrom']['attribut'] = [$attr['attribut'], implode(', ', $attr['table'])];
return false;
}
}
}
}
return true;
}
/**
* Check a "SELECT" sentence.
*
* @param array<int, array<string, mixed>> $select
* @param array $from
* @param bool $in
*
* @return bool
*/
public function checkedSelect($select, $from, $in = false)
{
$nb = count($select);
for ($i = 0; $i < $nb; ++$i) {
$attribut = $select[$i];
if ($attribut['base_expr'] != '*' && !preg_match('/\.\*$/', $attribut['base_expr'])) {
if ($attribut['expr_type'] == 'colref') {
if ($attr = $this->cutAttribute(trim($attribut['base_expr']), $from)) {
if (!$this->attributExistInTable($attr['attribut'], $attr['table'])) {
$this->error_sql['checkedSelect']['attribut'] = [$attr['attribut'], implode(', ', $attr['table'])];
return false;
}
} else {
if (isset($this->error_sql['returnNameTable'])) {
$this->error_sql['checkedSelect'] = $this->error_sql['returnNameTable'];
return false;
} else {
$this->error_sql['checkedSelect'] = false;
return false;
}
}
}
while (is_array($attribut['sub_tree'])) {
if ($attribut['expr_type'] === 'function' && in_array(strtoupper($attribut['base_expr']), $this->tested['unauthorized'])) {
$this->error_sql['checkedSelect']['function'] = $attribut['base_expr'];
return false;
}
$attribut = $attribut['sub_tree'][0];
}
} elseif ($in) {
$this->error_sql['checkedSelect']['*'] = false;
return false;
}
}
return true;
}
/**
* Check a "WHERE" sentence.
*
* @param array<int, array<string, mixed>> $where
* @param array $from
* @param string $sql
*
* @return bool
*/
public function checkedWhere($where, $from, $sql)
{
$nb = count($where);
for ($i = 0; $i < $nb; ++$i) {
$attribut = $where[$i];
if ($attribut['expr_type'] == 'colref') {
if ($attr = $this->cutAttribute(trim($attribut['base_expr']), $from)) {
if (!$this->attributExistInTable($attr['attribut'], $attr['table'])) {
$this->error_sql['checkedWhere']['attribut'] = [$attr['attribut'], implode(', ', $attr['table'])];
return false;
}
} else {
$this->error_sql['checkedWhere'] = $this->error_sql['returnNameTable'] ?? false;
return false;
}
} elseif ($attribut['expr_type'] == 'reserved') {
if ($attribut['base_expr'] !== 'EXISTS' || !isset($where[$i + 1]) || $where[$i + 1]['expr_type'] !== 'subquery') {
$this->error_sql['checkedWhere'] = $this->error_sql['returnNameTable'] ?? false;
return false;
}
} elseif ($attribut['expr_type'] == 'operator') {
if (!in_array(strtoupper($attribut['base_expr']), $this->tested['operator'])) {
$this->error_sql['checkedWhere']['operator'] = [$attribut['base_expr']];
return false;
}
} elseif ($attribut['expr_type'] == 'subquery') {
$tab = $attribut['sub_tree'];
return $this->validateParser($tab, true, $sql);
}
}
return true;
}
/**
* Check a "HAVING" sentence.
*
* @param array<int, array<string, mixed>> $having
* @param array $from
*
* @return bool
*/
public function checkedHaving($having, $from)
{
$nb = count($having);
for ($i = 0; $i < $nb; ++$i) {
$attribut = $having[$i];
if ($attribut['expr_type'] == 'colref') {
if ($attr = $this->cutAttribute(trim($attribut['base_expr']), $from)) {
if (!$this->attributExistInTable($attr['attribut'], $attr['table'])) {
$this->error_sql['checkedHaving']['attribut'] = [$attr['attribut'], implode(', ', $attr['table'])];
return false;
}
} else {
if (isset($this->error_sql['returnNameTable'])) {
$this->error_sql['checkedHaving'] = $this->error_sql['returnNameTable'];
return false;
} else {
$this->error_sql['checkedHaving'] = false;
return false;
}
}
}
if ($attribut['expr_type'] == 'operator') {
if (!in_array(strtoupper($attribut['base_expr']), $this->tested['operator'])) {
$this->error_sql['checkedHaving']['operator'] = [$attribut['base_expr']];
return false;
}
}
}
return true;
}
/**
* Check a "ORDER" sentence.
*
* @param array $order
* @param array $from
*
* @return bool
*/
public function checkedOrder($order, $from)
{
$order = $order[0];
if (array_key_exists('expression', $order) && $order['type'] == 'expression') {
if ($attr = $this->cutAttribute(trim($order['base_expr']), $from)) {
if (!$this->attributExistInTable($attr['attribut'], $attr['table'])) {
$this->error_sql['checkedOrder']['attribut'] = [$attr['attribut'], implode(', ', $attr['table'])];
return false;
}
} else {
if (isset($this->error_sql['returnNameTable'])) {
$this->error_sql['checkedOrder'] = $this->error_sql['returnNameTable'];
return false;
} else {
$this->error_sql['checkedOrder'] = false;
return false;
}
}
}
return true;
}
/**
* Check a "GROUP BY" sentence.
*
* @param array $group
* @param array $from
*
* @return bool
*/
public function checkedGroupBy($group, $from)
{
$group = $group[0];
if ($group['expr_type'] == 'colref') {
if ($attr = $this->cutAttribute(trim($group['base_expr']), $from)) {
if (!$this->attributExistInTable($attr['attribut'], $attr['table'])) {
$this->error_sql['checkedGroupBy']['attribut'] = [$attr['attribut'], implode(', ', $attr['table'])];
return false;
}
} else {
if (isset($this->error_sql['returnNameTable'])) {
$this->error_sql['checkedGroupBy'] = $this->error_sql['returnNameTable'];
return false;
} else {
$this->error_sql['checkedGroupBy'] = false;
return false;
}
}
}
return true;
}
/**
* Check a "LIMIT" sentence.
*
* @param array $limit
*
* @return bool
*/
public function checkedLimit($limit)
{
if (!preg_match('#^[0-9]+$#', trim($limit['offset'])) || !preg_match('#^[0-9]+$#', trim($limit['rowcount']))) {
$this->error_sql['checkedLimit'] = false;
return false;
}
return true;
}
}

61
classes/Risk.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class RiskCore.
*/
class RiskCore extends ObjectModel
{
public $id;
public $id_risk;
public $name;
public $color;
public $percent;
public static $definition = [
'table' => 'risk',
'primary' => 'id_risk',
'multilang' => true,
'fields' => [
'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isString', 'required' => true, 'size' => 20],
'color' => ['type' => self::TYPE_STRING, 'validate' => 'isColor', 'size' => 32],
'percent' => ['type' => self::TYPE_INT, 'validate' => 'isPercentage'],
],
];
/**
* Get fields.
*
* @return mixed
*/
public function getFields()
{
$this->validateFields();
$fields['id_risk'] = (int) $this->id_risk;
$fields['color'] = pSQL($this->color);
$fields['percent'] = (int) $this->percent;
return $fields;
}
/**
* Get Risks.
*
* @param int|null $idLang Language ID
*
* @return PrestaShopCollection
*/
public static function getRisks($idLang = null)
{
if (null === $idLang) {
$idLang = Context::getContext()->language->id;
}
$risks = new PrestaShopCollection('Risk', $idLang);
return $risks;
}
}

1273
classes/Search.php Normal file

File diff suppressed because it is too large Load Diff

64
classes/SearchEngine.php Normal file
View File

@@ -0,0 +1,64 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class SearchEngineCore.
*/
class SearchEngineCore extends ObjectModel
{
public $server;
public $getvar;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'search_engine',
'primary' => 'id_search_engine',
'fields' => [
'server' => ['type' => self::TYPE_STRING, 'validate' => 'isUrl', 'required' => true, 'size' => 64],
'getvar' => ['type' => self::TYPE_STRING, 'validate' => 'isModuleName', 'required' => true, 'size' => 16],
],
];
/**
* Get keywords.
*
* @param string $url
*
* @return bool|string
*/
public static function getKeywords($url)
{
$parsedUrl = @parse_url($url);
if (!isset($parsedUrl['host']) || !isset($parsedUrl['query'])) {
return false;
}
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT `server`, `getvar` FROM `' . _DB_PREFIX_ . 'search_engine`');
foreach ($result as $row) {
$host = &$row['server'];
$varname = &$row['getvar'];
if (strstr($parsedUrl['host'], $host)) {
$array = [];
preg_match('/[^a-z]' . $varname . '=.+\&/U', $parsedUrl['query'], $array);
if (empty($array[0])) {
preg_match('/[^a-z]' . $varname . '=.+$/', $parsedUrl['query'], $array);
}
if (empty($array[0])) {
return false;
}
$str = urldecode(str_replace('+', ' ', ltrim(substr(rtrim($array[0], '&'), strlen($varname) + 1), '=')));
if (!Validate::isMessage($str)) {
return false;
}
return $str;
}
}
return '';
}
}

View File

@@ -0,0 +1,280 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
class SmartyCustomCore extends Smarty
{
public function __construct()
{
parent::__construct();
$this->template_class = 'SmartyCustomTemplate';
}
/**
* Delete compiled template file (lazy delete if resource_name is not specified).
*
* @param string $resource_name template name
* @param string $compile_id compile id
* @param int $exp_time expiration time
*
* @return int number of template files deleted
*/
public function clearCompiledTemplate($resource_name = null, $compile_id = null, $exp_time = null)
{
if ($resource_name == null) {
Db::getInstance()->execute('REPLACE INTO `' . _DB_PREFIX_ . 'smarty_last_flush` (`type`, `last_flush`) VALUES (\'compile\', FROM_UNIXTIME(' . time() . '))');
return 0;
}
return parent::clearCompiledTemplate($resource_name, $compile_id, $exp_time);
}
/**
* Mark all template files to be regenerated.
*
* @param int $exp_time expiration time
* @param string $type resource type
*
* @return int number of cache files which needs to be updated
*/
public function clearAllCache($exp_time = null, $type = null)
{
Db::getInstance()->execute('REPLACE INTO `' . _DB_PREFIX_ . 'smarty_last_flush` (`type`, `last_flush`) VALUES (\'template\', FROM_UNIXTIME(' . time() . '))');
return $this->delete_from_lazy_cache('', null, null);
}
/**
* Mark file to be regenerated for a specific template.
*
* @param string $template_name template name
* @param string $cache_id cache id
* @param string $compile_id compile id
* @param int $exp_time expiration time
* @param string $type resource type
*
* @return int number of cache files which needs to be updated
*/
public function clearCache($template_name, $cache_id = null, $compile_id = null, $exp_time = null, $type = null)
{
return $this->delete_from_lazy_cache($template_name, $cache_id, $compile_id);
}
/**
* Check the compile cache needs to be invalidated (multi front + local cache compatible).
*/
public function check_compile_cache_invalidation()
{
static $last_flush = null;
if (!file_exists($this->getCompileDir() . 'last_flush')) {
@touch($this->getCompileDir() . 'last_flush', time());
} elseif (defined('_DB_PREFIX_')) {
if ($last_flush === null) {
$sql = 'SELECT UNIX_TIMESTAMP(last_flush) as last_flush FROM `' . _DB_PREFIX_ . 'smarty_last_flush` WHERE type=\'compile\'';
$last_flush = Db::getInstance()->getValue($sql, false);
}
if ((int) $last_flush && @filemtime($this->getCompileDir() . 'last_flush') < $last_flush) {
@touch($this->getCompileDir() . 'last_flush', time());
parent::clearCompiledTemplate();
}
}
}
/**
* {@inheritdoc}
*/
public function fetch($template = null, $cache_id = null, $compile_id = null, $parent = null, $display = false, $merge_tpl_vars = true, $no_output_filter = false)
{
$this->check_compile_cache_invalidation();
return parent::fetch($template, $cache_id, $compile_id, $parent);
}
/**
* {@inheritdoc}
*/
public function createTemplate($template, $cache_id = null, $compile_id = null, $parent = null, $do_clone = true)
{
$this->check_compile_cache_invalidation();
if ($this->caching) {
$this->check_template_invalidation($template, $cache_id, $compile_id);
}
return parent::createTemplate($template, $cache_id, $compile_id, $parent, $do_clone);
}
/**
* Handle the lazy template cache invalidation.
*
* @param string $template template name
* @param string|array|object|null $cache_id cache id
* @param string $compile_id compile id
*/
public function check_template_invalidation($template, $cache_id, $compile_id)
{
static $last_flush = null;
if (!file_exists($this->getCacheDir() . 'last_template_flush')) {
@touch($this->getCacheDir() . 'last_template_flush', time());
} elseif (defined('_DB_PREFIX_')) {
if ($last_flush === null) {
$sql = 'SELECT UNIX_TIMESTAMP(last_flush) as last_flush FROM `' . _DB_PREFIX_ . 'smarty_last_flush` WHERE type=\'template\'';
$last_flush = Db::getInstance()->getValue($sql, false);
}
if ((int) $last_flush && @filemtime($this->getCacheDir() . 'last_template_flush') < $last_flush) {
@touch($this->getCacheDir() . 'last_template_flush', time());
parent::clearAllCache();
} else {
if ($cache_id !== null && (is_object($cache_id) || is_array($cache_id))) {
$cache_id = null;
}
if ($this->is_in_lazy_cache($template, $cache_id, $compile_id) === false) {
// insert in cache before the effective cache creation to avoid nasty race condition
$this->insert_in_lazy_cache($template, $cache_id, $compile_id);
parent::clearCache($template, $cache_id, $compile_id);
}
}
}
}
/**
* Store the cache file path.
*
* @param string $filepath cache file path
* @param string $template template name
* @param string $cache_id cache id
* @param string $compile_id compile id
*/
public function update_filepath($filepath, $template, $cache_id, $compile_id)
{
$template_md5 = md5($template);
$sql = 'UPDATE `' . _DB_PREFIX_ . 'smarty_lazy_cache`
SET filepath=\'' . pSQL($filepath) . '\'
WHERE `template_hash`=\'' . pSQL($template_md5) . '\'';
$sql .= ' AND cache_id="' . pSQL((string) $cache_id) . '"';
if (strlen($compile_id) > 32) {
$compile_id = md5($compile_id);
}
$sql .= ' AND compile_id="' . pSQL((string) $compile_id) . '"';
Db::getInstance()->execute($sql, false);
}
/**
* Check if the current template is stored in the lazy cache
* Entry in the lazy cache = no need to regenerate the template.
*
* @param string $template template name
* @param string $cache_id cache id
* @param string $compile_id compile id
*
* @return bool
*/
public function is_in_lazy_cache($template, $cache_id, $compile_id)
{
static $is_in_lazy_cache = [];
$template_md5 = md5($template);
if (strlen($compile_id) > 32) {
$compile_id = md5($compile_id);
}
$key = md5($template_md5 . '-' . $cache_id . '-' . $compile_id);
if (isset($is_in_lazy_cache[$key])) {
return $is_in_lazy_cache[$key];
} else {
$sql = 'SELECT UNIX_TIMESTAMP(last_update) as last_update, filepath FROM `' . _DB_PREFIX_ . 'smarty_lazy_cache`
WHERE `template_hash`=\'' . pSQL($template_md5) . '\'';
$sql .= ' AND cache_id="' . pSQL((string) $cache_id) . '"';
$sql .= ' AND compile_id="' . pSQL((string) $compile_id) . '"';
$result = Db::getInstance()->getRow($sql, false);
// If the filepath is not yet set, it means the cache update is in progress in another process.
// In this case do not try to clear the cache again and tell to use the existing cache, if any
if ($result !== false && $result['filepath'] == '') {
// If the cache update is stalled for more than 1min, something should be wrong,
// remove the entry from the lazy cache
if ($result['last_update'] < time() - 60) {
$this->delete_from_lazy_cache($template, $cache_id, $compile_id);
}
$return = true;
} else {
if ($result === false
|| @filemtime($this->getCacheDir() . $result['filepath']) < $result['last_update']) {
$return = false;
} else {
$return = $result['filepath'];
}
}
$is_in_lazy_cache[$key] = $return;
}
return $return;
}
/**
* Insert the current template in the lazy cache.
*
* @param string $template template name
* @param string $cache_id cache id
* @param string $compile_id compile id
*
* @return bool
*/
public function insert_in_lazy_cache($template, $cache_id, $compile_id)
{
$template_md5 = md5($template);
$sql = 'INSERT IGNORE INTO `' . _DB_PREFIX_ . 'smarty_lazy_cache`
(`template_hash`, `cache_id`, `compile_id`, `last_update`)
VALUES (\'' . pSQL($template_md5) . '\'';
$sql .= ',"' . pSQL((string) $cache_id) . '"';
if (strlen($compile_id) > 32) {
$compile_id = md5($compile_id);
}
$sql .= ',"' . pSQL((string) $compile_id) . '"';
$sql .= ', FROM_UNIXTIME(' . time() . '))';
return Db::getInstance()->execute($sql, false);
}
/**
* Delete the current template from the lazy cache or the whole cache if no template name is given.
*
* @param string $template template name
* @param string|null $cache_id cache id
* @param string|null $compile_id compile id
*
* @return bool|int
*/
public function delete_from_lazy_cache($template, $cache_id, $compile_id)
{
if (!$template) {
return Db::getInstance()->execute('TRUNCATE TABLE `' . _DB_PREFIX_ . 'smarty_lazy_cache`', false);
}
$template_md5 = md5($template);
$sql = 'DELETE FROM `' . _DB_PREFIX_ . 'smarty_lazy_cache` WHERE template_hash=\'' . pSQL($template_md5) . '\'';
if ($cache_id != null) {
$sql .= ' AND cache_id LIKE "' . pSQL((string) $cache_id) . '%"';
}
if ($compile_id != null) {
if (strlen($compile_id) > 32) {
$compile_id = md5($compile_id);
}
$sql .= ' AND compile_id="' . pSQL((string) $compile_id) . '"';
}
Db::getInstance()->execute($sql, false);
return Db::getInstance()->Affected_Rows();
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* @property SmartyCustom|null $smarty
*/
class SmartyCustomTemplateCore extends Smarty_Internal_Template
{
public function fetch($template = null, $cache_id = null, $compile_id = null, $parent = null, $display = false, $merge_tpl_vars = true, $no_output_filter = false)
{
if ($this->smarty->caching) {
$tpl = parent::fetch($template, $cache_id, $compile_id, $parent);
if (property_exists($this, 'cached')) {
$filepath = str_replace($this->smarty->getCacheDir(), '', $this->cached->filepath);
if ($this->smarty->is_in_lazy_cache($this->template_resource, $this->cache_id, $this->compile_id) != $filepath) {
$this->smarty->update_filepath($filepath, $this->template_resource, $this->cache_id, $this->compile_id);
}
}
return $tpl;
} else {
return parent::fetch($template, $cache_id, $compile_id, $parent);
}
}
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
class SmartyDev extends Smarty
{
public function __construct()
{
parent::__construct();
$this->template_class = 'SmartyDevTemplate';
}
/**
* {@inheritdoc}
*/
public function fetch($template = null, $cache_id = null, $compile_id = null, $parent = null, $display = false, $merge_tpl_vars = true, $no_output_filter = false)
{
return "\n<!-- begin $template -->\n"
. parent::fetch($template, $cache_id, $compile_id, $parent)
. "\n<!-- end $template -->\n";
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* @property SmartyCustom|null $smarty
*/
class SmartyDevTemplateCore extends Smarty_Internal_Template
{
/**
* @param SmartyDevTemplateCore|null $template
* @param null $cache_id
* @param null $compile_id
* @param object $parent
* @param false $display
* @param bool $merge_tpl_vars
* @param false $no_output_filter
*
* @return string
*
* @throws SmartyException
*/
// @phpstan-ignore-next-line
public function fetch($template = null, $cache_id = null, $compile_id = null, $parent = null, $display = false, $merge_tpl_vars = true, $no_output_filter = false)
{
if (null !== $template) {
$tpl = $template->template_resource;
} else {
$tpl = $this->template_resource;
}
// @phpstan-ignore-next-line
$fetch = parent::fetch($template, $cache_id, $compile_id, $parent);
return "\n<!-- begin $tpl -->\n" . $fetch . "\n<!-- end $tpl -->\n";
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Used to delay loading of external classes with smarty->register_plugin.
*/
class SmartyLazyRegister
{
protected $registry = [];
protected static $instances = [];
/**
* Register a function or method to be dynamically called later.
*
* @param string|array $params function name or array(object name, method name)
*/
public function register($params)
{
if (is_array($params)) {
$this->registry[$params[1]] = $params;
} else {
$this->registry[$params] = $params;
}
}
public function isRegistered($params)
{
if (is_array($params)) {
$params = $params[1];
}
return isset($this->registry[$params]);
}
/**
* Dynamically call static function or method.
*
* @param string $name function name
* @param mixed $arguments function argument
*
* @return mixed function return
*/
public function __call($name, $arguments)
{
$item = $this->registry[$name];
// case 1: call to static method
// case 2 : call to static function
$args = [];
foreach ($arguments as $a => $argument) {
if ($a == 0) {
$args[] = $arguments[0];
} else {
$args[] = &$arguments[$a];
}
}
return call_user_func_array($item, $args);
}
public static function getInstance($smarty)
{
$hash = spl_object_hash($smarty);
if (!isset(self::$instances[$hash])) {
self::$instances[$hash] = new self();
}
return self::$instances[$hash];
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Override module templates easily.
*/
class SmartyResourceModuleCore extends Smarty_Resource_Custom
{
/**
* @var array<string>
*/
public $paths;
/**
* @var bool
*/
public $isAdmin;
public function __construct(array $paths, $isAdmin = false)
{
$this->paths = $paths;
$this->isAdmin = $isAdmin;
}
/**
* Fetch a template.
*
* @param string $name template name
* @param string $source template source
* @param int $mtime template modification timestamp (epoch)
*/
protected function fetch($name, &$source, &$mtime)
{
foreach ($this->paths as $path) {
if (Tools::file_exists_cache($file = $path . $name)) {
if (_PS_MODE_DEV_) {
$source = implode('', [
'<!-- begin ' . $file . ' -->',
file_get_contents($file),
'<!-- end ' . $file . ' -->',
]);
} else {
$source = file_get_contents($file);
}
$mtime = filemtime($file);
return;
}
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Override module templates easily.
*/
class SmartyResourceParentCore extends Smarty_Resource_Custom
{
/**
* @var array<string>
*/
public $paths;
public function __construct(array $paths)
{
$this->paths = $paths;
}
/**
* Fetch a template.
*
* @param string $name template name
* @param string $source template source
* @param int $mtime template modification timestamp (epoch)
*/
protected function fetch($name, &$source, &$mtime)
{
foreach ($this->paths as $path) {
if (Tools::file_exists_cache($file = $path . $name)) {
if (_PS_MODE_DEV_) {
$source = implode('', [
'<!-- begin ' . $file . ' -->',
file_get_contents($file),
'<!-- end ' . $file . ' -->',
]);
} else {
$source = file_get_contents($file);
}
$mtime = filemtime($file);
return;
}
}
}
}

View File

@@ -0,0 +1,191 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Determine the best existing template.
*/
class TemplateFinderCore
{
private $directories;
private $extension;
private $productListEntities = ['category', 'manufacturer', 'supplier'];
private $productListSearchEntities = ['search', 'price-drop', 'best-sale', 'prices-drop', 'best-sales', 'new-products'];
private $productEntities = ['product'];
private $brandListEntities = ['manufacturers', 'suppliers'];
public function __construct(array $directories, $extension)
{
$this->directories = $directories;
$this->extension = $extension;
}
public function getTemplate($template, $entity, $id, $locale)
{
$locale = (Validate::isLocale($locale)) ? $locale : '';
$templates = $this->getTemplateHierarchy($template, $entity, $id);
foreach ($this->directories as $dir) {
foreach ($templates as $tpl) {
if (!empty($locale) && is_file($dir . $locale . DIRECTORY_SEPARATOR . $tpl . $this->extension)) {
return $locale . DIRECTORY_SEPARATOR . $tpl . $this->extension;
}
if (is_file($dir . $tpl . $this->extension)) {
return $tpl . $this->extension;
}
if (is_file($dir . $tpl) && false !== strpos($tpl, $this->extension)) {
return $tpl;
}
}
}
throw new PrestaShopException('No template found for ' . $template);
}
private function getTemplateHierarchy($template, $entity, $id)
{
$entity = basename($entity ?? '');
$id = (int) $id;
if (in_array($entity, $this->getProductListEntities())) {
$templates = [
'catalog/listing/' . $entity . '-' . $id,
'catalog/listing/' . $entity,
$template,
'catalog/listing/product-list',
];
} elseif (in_array($entity, $this->getProductListSearchEntities())) {
$templates = [
'catalog/listing/' . $entity,
$template,
'catalog/listing/product-list',
];
} elseif (in_array($entity, $this->getProductEntities())) {
$templates = [
'catalog/' . $entity . '-' . $id,
$template,
'catalog/product',
];
} elseif (in_array($entity, $this->getBrandListEntities())) {
$templates = [
$template,
'catalog/brands',
];
} elseif ('cms' === $entity) {
$templates = [
'cms/page-' . $id,
$template,
'cms/page',
];
} elseif ('cms_category' === $entity) {
$templates = [
'cms/category-' . $id,
$template,
'cms/category',
];
} else {
$templates = [$template];
}
return array_unique($templates);
}
/**
* Get productListEntities.
*
* @return array
*/
public function getProductListEntities()
{
return $this->productListEntities;
}
/**
* Set productListEntities.
*
* @param array $productListEntities
*
* @return TemplateFinderCore
*/
public function setProductListEntities($productListEntities)
{
$this->productListEntities = $productListEntities;
return $this;
}
/**
* Get productListSearch.
*
* @return array
*/
public function getProductListSearchEntities()
{
return $this->productListSearchEntities;
}
/**
* Set productListSearch.
*
* @param array $productListSearchEntities
*
* @return TemplateFinderCore
*/
public function setProductListSearchEntities($productListSearchEntities)
{
$this->productListSearchEntities = $productListSearchEntities;
return $this;
}
/**
* Get productEntities.
*
* @return array
*/
public function getProductEntities()
{
return $this->productEntities;
}
/**
* Set productEntities.
*
* @param array $productEntities
*
* @return TemplateFinderCore
*/
public function setProductEntities($productEntities)
{
$this->productEntities = $productEntities;
return $this;
}
/**
* Get brandListEntities.
*
* @return array
*/
public function getBrandListEntities()
{
return $this->brandListEntities;
}
/**
* Set brandListEntities.
*
* @param array $brandListEntities
*
* @return TemplateFinderCore
*/
public function setBrandListEntities($brandListEntities)
{
$this->brandListEntities = $brandListEntities;
return $this;
}
}

788
classes/SpecificPrice.php Normal file
View File

@@ -0,0 +1,788 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
class SpecificPriceCore extends ObjectModel
{
public const ORDER_DEFAULT_FROM_QUANTITY = 1;
public const ORDER_DEFAULT_DATE = '0000-00-00 00:00:00';
public $id_product;
public $id_specific_price_rule = 0;
public $id_cart = 0;
public $id_product_attribute;
public $id_shop;
public $id_shop_group;
public $id_currency;
public $id_country;
public $id_group;
public $id_customer;
public $price;
public $from_quantity;
public $reduction;
public $reduction_tax = 1;
public $reduction_type;
public $from;
public $to;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'specific_price',
'primary' => 'id_specific_price',
'fields' => [
'id_shop_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_shop' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_cart' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_product' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_product_attribute' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_currency' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_specific_price_rule' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
'id_country' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_customer' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'price' => ['type' => self::TYPE_FLOAT, 'validate' => 'isNegativePrice', 'required' => true],
'from_quantity' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'required' => true],
'reduction' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true],
'reduction_tax' => ['type' => self::TYPE_INT, 'validate' => 'isBool', 'required' => true],
'reduction_type' => ['type' => self::TYPE_STRING, 'validate' => 'isReductionType', 'required' => true],
'from' => ['type' => self::TYPE_DATE, 'validate' => 'isDateFormat', 'required' => true],
'to' => ['type' => self::TYPE_DATE, 'validate' => 'isDateFormat', 'required' => true],
],
];
protected $webserviceParameters = [
'objectsNodeName' => 'specific_prices',
'objectNodeName' => 'specific_price',
'fields' => [
'id_shop_group' => ['xlink_resource' => 'shop_groups'],
'id_shop' => ['xlink_resource' => 'shops', 'required' => true],
'id_cart' => ['xlink_resource' => 'carts', 'required' => true],
'id_product' => ['xlink_resource' => 'products', 'required' => true],
'id_product_attribute' => ['xlink_resource' => 'product_attributes'],
'id_currency' => ['xlink_resource' => 'currencies', 'required' => true],
'id_country' => ['xlink_resource' => 'countries', 'required' => true],
'id_group' => ['xlink_resource' => 'groups', 'required' => true],
'id_customer' => ['xlink_resource' => 'customers', 'required' => true],
],
];
/**
* Local cache for getSpecificPrice function results.
*
* @var array
*/
protected static $_specificPriceCache = [];
/**
* Local cache which stores if a product could have an associated specific price.
*
* @var array
*/
protected static $_couldHaveSpecificPriceCache = [];
/**
* Store if the specific_price table contains any global rules in the productId columns
* i.e. if there is a product_id == 0 somewhere in the specific_price table.
*
* @var bool|null
*/
protected static $_hasGlobalProductRules = null;
/**
* Local cache for the filterOutField function. It stores the different existing values in the specific_price table
* for a given column name.
*
* @var array
*/
protected static $_filterOutCache = [];
/**
* Local cache for getPriority function.
*
* @var array
*/
protected static $_cache_priorities = [];
/**
* Local cache which stores if a given column name could have a value != 0 in the specific_price table
* i.e. if columnName != 0 somewhere in the specific_price table.
*
* @var array
*/
protected static $_no_specific_values = [];
protected static $psQtyDiscountOnCombination = null;
public static function resetStaticCache()
{
parent::resetStaticCache();
static::flushCache();
}
/**
* Flush local cache.
*/
public static function flushCache()
{
self::$_specificPriceCache = [];
self::$_couldHaveSpecificPriceCache = [];
self::$_hasGlobalProductRules = null;
self::$_filterOutCache = [];
self::$_cache_priorities = [];
self::$_no_specific_values = [];
self::$psQtyDiscountOnCombination = null;
Product::flushPriceCache();
}
public function add($autodate = true, $nullValues = false)
{
$specificPriceUsed = (bool) Configuration::getGlobalValue('PS_SPECIFIC_PRICE_FEATURE_ACTIVE');
if (!$specificPriceUsed) {
// Set cache of feature detachable to true
Configuration::updateGlobalValue('PS_SPECIFIC_PRICE_FEATURE_ACTIVE', '1');
}
if (parent::add($autodate, $nullValues)) {
// Flush cache when we adding a new specific price
$this->flushCache();
return true;
}
if (!$specificPriceUsed) {
// Restore previous PS_SPECIFIC_PRICE_FEATURE_ACTIVE state because the add operation failed
Configuration::updateGlobalValue('PS_SPECIFIC_PRICE_FEATURE_ACTIVE', false);
}
return false;
}
public function update($null_values = false)
{
if (parent::update($null_values)) {
// Flush cache when we updating a new specific price
$this->flushCache();
return true;
}
return false;
}
public function delete()
{
if (parent::delete()) {
// Flush cache when we deletind a new specific price
$this->flushCache();
// Refresh cache of feature detachable
Configuration::updateGlobalValue('PS_SPECIFIC_PRICE_FEATURE_ACTIVE', SpecificPrice::isCurrentlyUsed($this->def['table']));
return true;
}
return false;
}
public static function getByProductId($id_product, $id_product_attribute = false, $id_cart = false, $id_price_rule = null)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT *
FROM `' . _DB_PREFIX_ . 'specific_price`
WHERE `id_product` = ' . (int) $id_product .
($id_product_attribute ? ' AND id_product_attribute = ' . (int) $id_product_attribute : '') . '
AND id_cart = ' . (int) $id_cart .
($id_price_rule !== null ? ' AND id_specific_price_rule = ' . (int) $id_price_rule : ''));
}
public static function deleteByIdCart($id_cart, $id_product = false, $id_product_attribute = false)
{
return Db::getInstance()->execute('
DELETE FROM `' . _DB_PREFIX_ . 'specific_price`
WHERE id_cart=' . (int) $id_cart .
($id_product ? ' AND id_product=' . (int) $id_product . ' AND id_product_attribute=' . (int) $id_product_attribute : ''));
}
public static function getIdsByProductId($id_product, $id_product_attribute = false, $id_cart = 0)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT `id_specific_price`
FROM `' . _DB_PREFIX_ . 'specific_price`
WHERE `id_product` = ' . (int) $id_product .
($id_product_attribute !== false ? ' AND id_product_attribute = ' . (int) $id_product_attribute : '') . '
AND id_cart = ' . (int) $id_cart);
}
/**
* score generation for quantity discount.
*/
protected static function _getScoreQuery($id_product, $id_shop, $id_currency, $id_country, $id_group, $id_customer)
{
$select = '(';
$priority = SpecificPrice::getPriority($id_product);
foreach (array_reverse($priority) as $k => $field) {
if (!empty($field)) {
$select .= ' IF (`' . bqSQL($field) . '` = ' . (int) $$field . ', ' . 2 ** ($k + 1) . ', 0) + ';
}
}
return rtrim($select, ' +') . ') AS `score`';
}
public static function getPriority($id_product)
{
if (!SpecificPrice::isFeatureActive()) {
return explode(';', Configuration::get('PS_SPECIFIC_PRICE_PRIORITIES'));
}
if (!isset(self::$_cache_priorities[(int) $id_product])) {
self::$_cache_priorities[(int) $id_product] = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
SELECT `priority`, `id_specific_price_priority`
FROM `' . _DB_PREFIX_ . 'specific_price_priority`
WHERE `id_product` = ' . (int) $id_product . '
ORDER BY `id_specific_price_priority` DESC
');
}
$priority = self::$_cache_priorities[(int) $id_product];
if (!$priority) {
$priority = Configuration::get('PS_SPECIFIC_PRICE_PRIORITIES');
}
$priority = 'id_customer;' . $priority;
return explode(';', $priority);
}
/**
* Remove or add a field value to a query if values are present in the database (cache friendly).
*
* @param string $field_name
* @param int $field_value
* @param int $threshold
*
* @return string
*
* @throws PrestaShopDatabaseException
*/
protected static function filterOutField($field_name, $field_value, $threshold = 1000)
{
$name = Db::getInstance()->escape($field_name, false, true);
$query_extra = 'AND `' . $name . '` = 0 ';
if ($field_value == 0 || array_key_exists($field_name, self::$_no_specific_values)) {
return $query_extra;
}
$key_cache = __FUNCTION__ . '-' . $field_name . '-' . $threshold;
$specific_list = [];
if (!array_key_exists($key_cache, self::$_filterOutCache)) {
// Check if a specific price with this key exists
$query = 'SELECT 1 FROM `' . _DB_PREFIX_ . 'specific_price` WHERE `' . $name . '` != 0';
$has_product_specific_price = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query);
if ($has_product_specific_price == 0) {
self::$_no_specific_values[$field_name] = true;
return $query_extra;
}
// Fetch the approximate count of specific price. explain can be 100x faster than count.
$query_count = 'EXPLAIN SELECT COUNT(DISTINCT `' . $name . '`) FROM `' . _DB_PREFIX_ . 'specific_price` WHERE `' . $name . '` != 0';
$specific_count_result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($query_count);
$specific_count = $specific_count_result['rows'];
if ($specific_count < $threshold) {
$query = 'SELECT DISTINCT `' . $name . '` FROM `' . _DB_PREFIX_ . 'specific_price` WHERE `' . $name . '` != 0';
$tmp_specific_list = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query);
foreach ($tmp_specific_list as $value) {
$specific_list[] = $value[$field_name];
}
}
self::$_filterOutCache[$key_cache] = $specific_list;
} else {
$specific_list = self::$_filterOutCache[$key_cache];
}
// $specific_list is empty if the threshold is reached
if (empty($specific_list) || in_array($field_value, $specific_list)) {
if ($name == 'id_product' && !self::$_hasGlobalProductRules) {
$query_extra = 'AND `' . $name . '` = ' . (int) $field_value . ' ';
} else {
$query_extra = 'AND `' . $name . '` ' . self::formatIntInQuery(0, $field_value) . ' ';
}
}
return $query_extra;
}
/**
* Remove or add useless fields value depending on the values in the database (cache friendly).
*
* @param int|null $id_product
* @param int|null $id_product_attribute
* @param int|null $id_cart
* @param string|null $beginning
* @param string|null $ending
*
* @return string
*/
protected static function computeExtraConditions($id_product, $id_product_attribute, $id_customer, $id_cart, $beginning = null, $ending = null)
{
$first_date = date('Y-m-d 00:00:00');
$last_date = date('Y-m-d 23:59:59');
$now = date('Y-m-d H:i:00');
if ($beginning === null) {
$beginning = $now;
}
if ($ending === null) {
$ending = $now;
}
$id_customer = (int) $id_customer;
$id_cart = (int) $id_cart;
$query_extra = '';
if ($id_product !== null) {
$query_extra .= self::filterOutField('id_product', $id_product);
}
if ($id_customer !== null) {
$query_extra .= self::filterOutField('id_customer', $id_customer);
}
if ($id_product_attribute !== null) {
$query_extra .= self::filterOutField('id_product_attribute', $id_product_attribute);
}
$query_extra .= self::filterOutField('id_cart', $id_cart);
if ($ending == $now && $beginning == $now) {
$key = __FUNCTION__ . '-' . $first_date . '-' . $last_date;
if (!array_key_exists($key, self::$_filterOutCache)) {
$query_from_count = 'SELECT 1 FROM `' . _DB_PREFIX_ . 'specific_price` WHERE `from` BETWEEN \'' . $first_date . '\' AND \'' . $last_date . '\'';
$from_specific_count = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query_from_count);
$query_to_count = 'SELECT 1 FROM `' . _DB_PREFIX_ . 'specific_price` WHERE `to` BETWEEN \'' . $first_date . '\' AND \'' . $last_date . '\'';
$to_specific_count = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query_to_count);
self::$_filterOutCache[$key] = [$from_specific_count, $to_specific_count];
} else {
list($from_specific_count, $to_specific_count) = self::$_filterOutCache[$key];
}
} else {
$from_specific_count = $to_specific_count = 1;
}
// if the from and to is not reached during the current day, just change $ending & $beginning to any date of the day to improve the cache
if (!$from_specific_count && !$to_specific_count) {
$ending = $beginning = $first_date;
}
$db = Db::getInstance();
$beginning = $db->escape($beginning);
$ending = $db->escape($ending);
$query_extra .= ' AND (`from` = \'0000-00-00 00:00:00\' OR \'' . $beginning . '\' >= `from`)'
. ' AND (`to` = \'0000-00-00 00:00:00\' OR \'' . $ending . '\' <= `to`)';
return $query_extra;
}
protected static function formatIntInQuery($first_value, $second_value)
{
$first_value = (int) $first_value;
$second_value = (int) $second_value;
if ($first_value != $second_value) {
return 'IN (' . $first_value . ', ' . $second_value . ')';
} else {
return ' = ' . $first_value;
}
}
/**
* Check if the given product could have a specific price.
*
* @param int $idProduct
*
* @return bool
*/
final protected static function couldHaveSpecificPrice($idProduct)
{
if (self::$_hasGlobalProductRules === null) {
$queryHasGlobalRule = 'SELECT 1 FROM `' . _DB_PREFIX_ . 'specific_price` WHERE id_product = 0';
$row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($queryHasGlobalRule);
self::$_hasGlobalProductRules = !empty($row);
}
if (self::$_hasGlobalProductRules) {
return true;
}
if (!array_key_exists($idProduct, self::$_couldHaveSpecificPriceCache)) {
$query = 'SELECT 1 FROM `' . _DB_PREFIX_ . 'specific_price` WHERE id_product = ' . (int) $idProduct;
$row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($query);
self::$_couldHaveSpecificPriceCache[$idProduct] = !empty($row);
}
return self::$_couldHaveSpecificPriceCache[$idProduct];
}
/**
* Compute the cache key by setting to 0 the fields which doesn't have any specific values in the DB.
*
* @param int $id_product
* @param int $id_shop
* @param int $id_currency
* @param int $id_country
* @param int $id_group
* @param int $quantity
* @param int $id_product_attribute
* @param int $id_customer
* @param int $id_cart
* @param int $real_quantity
*
* @return string
*/
final protected static function computeKey(
$id_product,
$id_shop,
$id_currency,
$id_country,
$id_group,
$quantity,
$id_product_attribute,
$id_customer,
$id_cart,
$real_quantity
) {
if (self::$_no_specific_values !== null) {
// $_no_specific_values contains the fieldName from the DB which don't have values != 0
// So it's ok the set the value to 0 for those fields to improve the cache efficiency
// Note that the variableName from the DB needs to match the function args name
// I.e. if the computeKey args are converted at some point in camelCase, we will need to introduce a
// snakeCase to camelCase conversion of $variableName
foreach (array_keys(self::$_no_specific_values) as $variableName) {
${$variableName} = 0;
}
}
return (int) $id_product . '-' . (int) $id_shop . '-' . (int) $id_currency . '-' . (int) $id_country . '-' .
(int) $id_group . '-' . (int) $quantity . '-' . (int) $id_product_attribute . '-' . (int) $id_cart . '-' .
(int) $id_customer . '-' . (int) $real_quantity;
}
/**
* Returns the specificPrice information related to a given productId and context.
*
* @param int $id_product
* @param int $id_shop
* @param int $id_currency
* @param int $id_country
* @param int $id_group
* @param int $quantity
* @param int $id_product_attribute
* @param int $id_customer
* @param int $id_cart
* @param int $real_quantity
*
* @return array
*/
public static function getSpecificPrice(
$id_product,
$id_shop,
$id_currency,
$id_country,
$id_group,
$quantity,
$id_product_attribute = null,
$id_customer = 0,
$id_cart = 0,
$real_quantity = 0
) {
if (!SpecificPrice::isFeatureActive()) {
return [];
}
/*
* The date is not taken into account for the cache, but this is for the better because it keeps the consistency
* for the whole script.
* The price must not change between the top and the bottom of the page
*/
if (!self::couldHaveSpecificPrice($id_product)) {
return [];
}
if (static::$psQtyDiscountOnCombination === null) {
static::$psQtyDiscountOnCombination = Configuration::get('PS_QTY_DISCOUNT_ON_COMBINATION');
// no need to compute the key the first time the function is called, we know the cache has not
// been computed yet
$key = null;
} else {
$key = self::computeKey(
$id_product,
$id_shop,
$id_currency,
$id_country,
$id_group,
$quantity,
$id_product_attribute,
$id_customer,
$id_cart,
$real_quantity
);
}
if (!array_key_exists($key, self::$_specificPriceCache)) {
$query_extra = self::computeExtraConditions($id_product, $id_product_attribute, $id_customer, $id_cart);
if ($key === null) {
// compute the key after calling computeExtraConditions as it initializes some useful cache
$key = self::computeKey(
$id_product,
$id_shop,
$id_currency,
$id_country,
$id_group,
$quantity,
$id_product_attribute,
$id_customer,
$id_cart,
$real_quantity
);
}
$query = '
SELECT *, ' . SpecificPrice::_getScoreQuery($id_product, $id_shop, $id_currency, $id_country, $id_group, $id_customer) . '
FROM `' . _DB_PREFIX_ . 'specific_price`
WHERE
`id_shop` ' . self::formatIntInQuery(0, $id_shop) . ' AND
`id_currency` ' . self::formatIntInQuery(0, $id_currency) . ' AND
`id_country` ' . self::formatIntInQuery(0, $id_country) . ' AND
`id_group` ' . self::formatIntInQuery(0, $id_group) . ' ' . $query_extra . '
AND IF(`from_quantity` > 1, `from_quantity`, 0) <= ';
$query .= (static::$psQtyDiscountOnCombination || !$id_cart || !$real_quantity) ? (int) $quantity : max(1, (int) $real_quantity);
$query .= ' ORDER BY `id_product_attribute` DESC, `id_cart` DESC, `from_quantity` DESC, `id_specific_price_rule` ASC, `score` DESC, `to` DESC, `from` DESC';
self::$_specificPriceCache[$key] = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($query);
}
return self::$_specificPriceCache[$key];
}
/**
* Truncate the specific price priorities.
*
* @return bool
*/
public static function deletePriorities()
{
return Db::getInstance()->execute('
TRUNCATE `' . _DB_PREFIX_ . 'specific_price_priority`
');
}
public static function setSpecificPriority($id_product, $priorities)
{
$value = '';
foreach ($priorities as $priority) {
$value .= pSQL($priority) . ';';
}
return Db::getInstance()->execute('
INSERT INTO `' . _DB_PREFIX_ . 'specific_price_priority` (`id_product`, `priority`)
VALUES (' . (int) $id_product . ',\'' . pSQL(rtrim($value, ';')) . '\')
ON DUPLICATE KEY UPDATE `priority` = \'' . pSQL(rtrim($value, ';')) . '\'
');
}
public static function getQuantityDiscounts($id_product, $id_shop, $id_currency, $id_country, $id_group, $id_product_attribute = null, $all_combinations = false, $id_customer = 0)
{
if (!SpecificPrice::isFeatureActive()) {
return [];
}
$query_extra = self::computeExtraConditions($id_product, (!$all_combinations) ? $id_product_attribute : null, $id_customer, null);
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT *,
' . SpecificPrice::_getScoreQuery($id_product, $id_shop, $id_currency, $id_country, $id_group, $id_customer) . '
FROM `' . _DB_PREFIX_ . 'specific_price`
WHERE
`id_shop` ' . self::formatIntInQuery(0, $id_shop) . ' AND
`id_currency` ' . self::formatIntInQuery(0, $id_currency) . ' AND
`id_country` ' . self::formatIntInQuery(0, $id_country) . ' AND
`id_group` ' . self::formatIntInQuery(0, $id_group) . ' ' . $query_extra . '
ORDER BY `from_quantity` ASC, `id_specific_price_rule` ASC, `score` DESC, `to` DESC, `from` DESC
', false, false);
$targeted_prices = [];
$last_quantity = [];
while ($specific_price = Db::getInstance()->nextRow($result)) {
if (!isset($last_quantity[(int) $specific_price['id_product_attribute']])) {
$last_quantity[(int) $specific_price['id_product_attribute']] = $specific_price['from_quantity'];
} elseif ($last_quantity[(int) $specific_price['id_product_attribute']] == $specific_price['from_quantity']) {
continue;
}
$last_quantity[(int) $specific_price['id_product_attribute']] = $specific_price['from_quantity'];
if ($specific_price['from_quantity'] > 1) {
$targeted_prices[] = $specific_price;
}
}
return $targeted_prices;
}
public static function getQuantityDiscount($id_product, $id_shop, $id_currency, $id_country, $id_group, $quantity, $id_product_attribute = null, $id_customer = 0)
{
if (!SpecificPrice::isFeatureActive()) {
return [];
}
$query_extra = self::computeExtraConditions($id_product, $id_product_attribute, $id_customer, null);
return Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
SELECT *,
' . SpecificPrice::_getScoreQuery($id_product, $id_shop, $id_currency, $id_country, $id_group, $id_customer) . '
FROM `' . _DB_PREFIX_ . 'specific_price`
WHERE
`id_shop` ' . self::formatIntInQuery(0, $id_shop) . ' AND
`id_currency` ' . self::formatIntInQuery(0, $id_currency) . ' AND
`id_country` ' . self::formatIntInQuery(0, $id_country) . ' AND
`id_group` ' . self::formatIntInQuery(0, $id_group) . ' AND
`from_quantity` >= ' . (int) $quantity . ' ' . $query_extra . '
ORDER BY `from_quantity` DESC, `score` DESC, `to` DESC, `from` DESC
');
}
public static function getProductIdByDate($id_shop, $id_currency, $id_country, $id_group, $beginning, $ending, $id_customer = 0, $with_combination_id = false)
{
if (!SpecificPrice::isFeatureActive()) {
return [];
}
$query_extra = self::computeExtraConditions(null, null, $id_customer, null, $beginning, $ending);
$results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT `id_product`, `id_product_attribute`
FROM `' . _DB_PREFIX_ . 'specific_price`
WHERE `id_shop` ' . self::formatIntInQuery(0, $id_shop) . ' AND
`id_currency` ' . self::formatIntInQuery(0, $id_currency) . ' AND
`id_country` ' . self::formatIntInQuery(0, $id_country) . ' AND
`id_group` ' . self::formatIntInQuery(0, $id_group) . ' AND
`from_quantity` = 1 AND
`reduction` > 0
' . $query_extra);
$ids_product = [];
foreach ($results as $value) {
$ids_product[] = $with_combination_id ?
[
'id_product' => (int) $value['id_product'],
'id_product_attribute' => (int) $value['id_product_attribute'],
] : (int) $value['id_product'];
}
return $ids_product;
}
/**
* Delete a product from its id.
*
* @param int $id_product
*
* @return bool
*/
public static function deleteByProductId($id_product)
{
if (Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'specific_price` WHERE `id_product` = ' . (int) $id_product)) {
// Refresh cache of feature detachable
Configuration::updateGlobalValue('PS_SPECIFIC_PRICE_FEATURE_ACTIVE', SpecificPrice::isCurrentlyUsed('specific_price'));
return true;
}
return false;
}
/**
* Duplicate a product.
*
* @param bool|int $id_product The product ID to duplicate, false when duplicating the current product
* @param array $combination_associations Associations between the ids of base combinations and their duplicates
*
* @return bool
*/
public function duplicate($id_product = false, array $combination_associations = []): bool
{
if ($id_product) {
$this->id_product = (int) $id_product;
}
if ($this->id_product_attribute && isset($combination_associations[$this->id_product_attribute])) {
$this->id_product_attribute = (int) $combination_associations[$this->id_product_attribute];
}
unset($this->id);
// specific price row may already have been created for catalog specific price rule
if (static::exists(
$this->id_product,
$this->id_product_attribute,
$this->id_shop,
$this->id_group,
$this->id_country,
$this->id_currency,
$this->id_customer,
$this->from_quantity,
$this->from,
$this->to,
$this->id_specific_price_rule != 0
)) {
return true;
}
return $this->add();
}
/**
* This method is allow to know if a feature is used or active.
*
* @return bool
*/
public static function isFeatureActive()
{
return (bool) Configuration::get('PS_SPECIFIC_PRICE_FEATURE_ACTIVE');
}
/**
* Check if a specific price exists based on given parameters and return the specific rule id.
*
* @param int $id_product
* @param int $id_product_attribute Set at 0 when the specific price was set for all attributes
* @param int $id_shop Set at 0 when the specific price was set for all shops
* @param int $id_group Set at 0 when the specific price was set for all groups
* @param int $id_country Set at 0 when the specific price was set for all countries
* @param int $id_currency Set at 0 when the specific price was set for all currencies
* @param int $id_customer Set at 0 when the specific price was set for all customers
* @param int $from_quantity The starting quantity for which the specific price is applied
* @param string $from Date from which the specific price start. 0000-00-00 00:00:00 if no starting date
* @param string $to Date from which the specific price end. 0000-00-00 00:00:00 if no ending date
* @param bool $rule if a specific price rule (from specific_price_rule) was set or not
* @param int|null $id_cart if a specific cart was set or not (default: null no additional check is performed)
*
* @return int The specific rule id, 0 if no corresponding rule found
*/
public static function exists($id_product, $id_product_attribute, $id_shop, $id_group, $id_country, $id_currency, $id_customer, $from_quantity, $from, $to, $rule = false, $id_cart = null)
{
$rule = ' AND `id_specific_price_rule`' . (!$rule ? '=0' : '!=0');
if (null !== $id_cart) {
$rule .= ' AND id_cart = ' . (int) $id_cart;
}
return (int) Db::getInstance()->getValue('SELECT `id_specific_price`
FROM ' . _DB_PREFIX_ . 'specific_price
WHERE `id_product`=' . (int) $id_product . ' AND
`id_product_attribute`=' . (int) $id_product_attribute . ' AND
`id_shop`=' . (int) $id_shop . ' AND
`id_group`=' . (int) $id_group . ' AND
`id_country`=' . (int) $id_country . ' AND
`id_currency`=' . (int) $id_currency . ' AND
`id_customer`=' . (int) $id_customer . ' AND
`from_quantity`=' . (int) $from_quantity . ' AND
`from` >= \'' . pSQL($from) . '\' AND
`to` <= \'' . pSQL($to) . '\'' . $rule);
}
}

View File

@@ -0,0 +1,310 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
class SpecificPriceRuleCore extends ObjectModel
{
public $name;
public $id_shop;
public $id_currency;
public $id_country;
public $id_group;
public $from_quantity;
public $price;
public $reduction;
public $reduction_tax;
public $reduction_type;
public $from;
public $to;
protected static $rules_application_enable = true;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'specific_price_rule',
'primary' => 'id_specific_price_rule',
'fields' => [
'name' => ['type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'required' => true, 'size' => 255],
'id_shop' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_country' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_currency' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'from_quantity' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'required' => true],
'price' => ['type' => self::TYPE_FLOAT, 'validate' => 'isNegativePrice', 'required' => true],
'reduction' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true],
'reduction_tax' => ['type' => self::TYPE_INT, 'validate' => 'isBool', 'required' => true],
'reduction_type' => ['type' => self::TYPE_STRING, 'validate' => 'isReductionType', 'required' => true],
'from' => ['type' => self::TYPE_DATE, 'validate' => 'isDateFormat', 'required' => false],
'to' => ['type' => self::TYPE_DATE, 'validate' => 'isDateFormat', 'required' => false],
],
];
protected $webserviceParameters = [
'objectsNodeName' => 'specific_price_rules',
'objectNodeName' => 'specific_price_rule',
'fields' => [
'id_shop' => ['xlink_resource' => 'shops', 'required' => true],
'id_country' => ['xlink_resource' => 'countries', 'required' => true],
'id_currency' => ['xlink_resource' => 'currencies', 'required' => true],
'id_group' => ['xlink_resource' => 'groups', 'required' => true],
],
];
/**
* @return bool
*
* @throws PrestaShopException
*/
public function delete()
{
$this->deleteConditions();
Db::getInstance()->execute('DELETE FROM ' . _DB_PREFIX_ . 'specific_price WHERE id_specific_price_rule=' . (int) $this->id);
return (bool) parent::delete();
}
public function deleteConditions()
{
$ids_condition_group = Db::getInstance()->executeS('SELECT id_specific_price_rule_condition_group
FROM ' . _DB_PREFIX_ . 'specific_price_rule_condition_group
WHERE id_specific_price_rule=' . (int) $this->id);
if ($ids_condition_group) {
foreach ($ids_condition_group as $row) {
Db::getInstance()->delete('specific_price_rule_condition_group', 'id_specific_price_rule_condition_group=' . (int) $row['id_specific_price_rule_condition_group']);
Db::getInstance()->delete('specific_price_rule_condition', 'id_specific_price_rule_condition_group=' . (int) $row['id_specific_price_rule_condition_group']);
}
}
}
public static function disableAnyApplication()
{
SpecificPriceRule::$rules_application_enable = false;
}
public static function enableAnyApplication()
{
SpecificPriceRule::$rules_application_enable = true;
}
public function addConditions($conditions)
{
if (!is_array($conditions)) {
return;
}
$result = Db::getInstance()->insert('specific_price_rule_condition_group', [
'id_specific_price_rule' => (int) $this->id,
]);
if (!$result) {
return false;
}
$id_specific_price_rule_condition_group = (int) Db::getInstance()->Insert_ID();
foreach ($conditions as $condition) {
$result = Db::getInstance()->insert('specific_price_rule_condition', [
'id_specific_price_rule_condition_group' => (int) $id_specific_price_rule_condition_group,
'type' => pSQL($condition['type']),
'value' => (float) $condition['value'],
]);
if (!$result) {
return false;
}
}
return true;
}
public function apply($products = false)
{
if (!SpecificPriceRule::$rules_application_enable) {
return;
}
$this->resetApplication($products);
$products = $this->getAffectedProducts($products);
foreach ($products as $product) {
SpecificPriceRule::applyRuleToProduct((int) $this->id, (int) $product['id_product'], (int) $product['id_product_attribute']);
}
}
public function resetApplication($products = false)
{
$where = '';
if ($products && count($products)) {
$where .= ' AND id_product IN (' . implode(', ', array_map('intval', $products)) . ')';
}
return Db::getInstance()->execute('DELETE FROM ' . _DB_PREFIX_ . 'specific_price WHERE id_specific_price_rule=' . (int) $this->id . $where);
}
/**
* @param array|bool $products
*/
public static function applyAllRules($products = false)
{
if (!SpecificPriceRule::$rules_application_enable) {
return;
}
/** @var array<SpecificPriceRule> $rules */
$rules = new PrestaShopCollection('SpecificPriceRule');
foreach ($rules as $rule) {
$rule->apply($products);
}
}
public function getConditions()
{
$conditions = Db::getInstance()->executeS(
'
SELECT g.*, c.*
FROM ' . _DB_PREFIX_ . 'specific_price_rule_condition_group g
LEFT JOIN ' . _DB_PREFIX_ . 'specific_price_rule_condition c
ON (c.id_specific_price_rule_condition_group = g.id_specific_price_rule_condition_group)
WHERE g.id_specific_price_rule=' . (int) $this->id
);
$conditions_group = [];
if ($conditions) {
foreach ($conditions as &$condition) {
if ($condition['type'] == 'attribute') {
$condition['id_attribute_group'] = Db::getInstance()->getValue('SELECT id_attribute_group
FROM ' . _DB_PREFIX_ . 'attribute
WHERE id_attribute=' . (int) $condition['value']);
} elseif ($condition['type'] == 'feature') {
$condition['id_feature'] = Db::getInstance()->getValue('SELECT id_feature
FROM ' . _DB_PREFIX_ . 'feature_value
WHERE id_feature_value=' . (int) $condition['value']);
}
$conditions_group[(int) $condition['id_specific_price_rule_condition_group']][] = $condition;
}
}
return $conditions_group;
}
/**
* Return the product list affected by this specific rule.
*
* @param bool|array $products products list limitation
*
* @return array affected products list IDs
*
* @throws PrestaShopDatabaseException
*/
public function getAffectedProducts($products = false)
{
$conditions_group = $this->getConditions();
$shop_id = $this->id_shop ?: Context::getContext()->shop->id;
$result = [];
if ($conditions_group) {
foreach ($conditions_group as $condition_group) {
// Base request
$query = new DbQuery();
$query->select('p.`id_product`')
->from('product', 'p')
->leftJoin('product_shop', 'ps', 'p.`id_product` = ps.`id_product`')
->where('ps.id_shop = ' . (int) $shop_id);
$attributes_join_added = false;
// Add the conditions
foreach ($condition_group as $id_condition => $condition) {
if ($condition['type'] == 'attribute') {
if (!$attributes_join_added) {
$query->select('pa.`id_product_attribute`')
->leftJoin('product_attribute', 'pa', 'p.`id_product` = pa.`id_product`')
->join(Shop::addSqlAssociation('product_attribute', 'pa', false));
$attributes_join_added = true;
}
$query->leftJoin('product_attribute_combination', 'pac' . (int) $id_condition, 'pa.`id_product_attribute` = pac' . (int) $id_condition . '.`id_product_attribute`')
->where('pac' . (int) $id_condition . '.`id_attribute` = ' . (int) $condition['value']);
} elseif ($condition['type'] == 'manufacturer') {
$query->where('p.id_manufacturer = ' . (int) $condition['value']);
} elseif ($condition['type'] == 'category') {
$query->leftJoin('category_product', 'cp' . (int) $id_condition, 'p.`id_product` = cp' . (int) $id_condition . '.`id_product`')
->where('cp' . (int) $id_condition . '.id_category = ' . (int) $condition['value']);
} elseif ($condition['type'] == 'supplier') {
$query->where('EXISTS(
SELECT
`ps' . (int) $id_condition . '`.`id_product`
FROM
`' . _DB_PREFIX_ . 'product_supplier` `ps' . (int) $id_condition . '`
WHERE
`p`.`id_product` = `ps' . (int) $id_condition . '`.`id_product`
AND `ps' . (int) $id_condition . '`.`id_supplier` = ' . (int) $condition['value'] . '
)');
} elseif ($condition['type'] == 'feature') {
$query->leftJoin('feature_product', 'fp' . (int) $id_condition, 'p.`id_product` = fp' . (int) $id_condition . '.`id_product`')
->where('fp' . (int) $id_condition . '.`id_feature_value` = ' . (int) $condition['value']);
}
}
// Products limitation
if ($products && count($products)) {
$query->where('p.`id_product` IN (' . implode(', ', array_map('intval', $products)) . ')');
}
// Force the column id_product_attribute if not requested
if (!$attributes_join_added) {
$query->select('NULL as `id_product_attribute`');
}
// Merge previous result to current results
$result = array_merge($result, Db::getInstance()->executeS($query));
}
// Remove duplicate after the array_merge
$result = array_unique($result, SORT_REGULAR);
} else {
// All products without conditions
if ($products && count($products)) {
if (!SpecificPrice::getByProductId(0, false, false, (int) $this->id)) {
$query = new DbQuery();
$query->select('p.`id_product`')
->select('NULL as `id_product_attribute`')
->from('product', 'p')
->leftJoin('product_shop', 'ps', 'p.`id_product` = ps.`id_product`')
->where('ps.id_shop = ' . (int) $shop_id);
$query->where('p.`id_product` IN (' . implode(', ', array_map('intval', $products)) . ')');
$result = Db::getInstance()->executeS($query);
}
} else {
$result = [['id_product' => 0, 'id_product_attribute' => null]];
}
}
return $result;
}
public static function applyRuleToProduct($id_rule, $id_product, $id_product_attribute = null)
{
$rule = new SpecificPriceRule((int) $id_rule);
if (!Validate::isLoadedObject($rule) || !Validate::isUnsignedInt($id_product)) {
return false;
}
$specific_price = new SpecificPrice();
$specific_price->id_specific_price_rule = (int) $rule->id;
$specific_price->id_product = (int) $id_product;
$specific_price->id_product_attribute = (int) $id_product_attribute;
$specific_price->id_customer = 0;
$specific_price->id_shop = (int) $rule->id_shop;
$specific_price->id_country = (int) $rule->id_country;
$specific_price->id_currency = (int) $rule->id_currency;
$specific_price->id_group = (int) $rule->id_group;
$specific_price->from_quantity = (int) $rule->from_quantity;
$specific_price->price = (float) $rule->price;
$specific_price->reduction_type = $rule->reduction_type;
$specific_price->reduction_tax = $rule->reduction_tax;
$specific_price->reduction = ($rule->reduction_type == 'percentage' ? $rule->reduction / 100 : (float) $rule->reduction);
$specific_price->from = $rule->from;
$specific_price->to = $rule->to;
return $specific_price->add();
}
}

250
classes/State.php Normal file
View File

@@ -0,0 +1,250 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class StateCore.
*/
class StateCore extends ObjectModel
{
/** @var int Country id which state belongs */
public $id_country;
/** @var int Zone id which state belongs */
public $id_zone;
/** @var string 2 letters iso code */
public $iso_code;
/** @var string Name */
public $name;
/** @var bool Status for delivery */
public $active = true;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'state',
'primary' => 'id_state',
'fields' => [
'id_country' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_zone' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'iso_code' => ['type' => self::TYPE_STRING, 'validate' => 'isStateIsoCode', 'required' => true, 'size' => 7],
'name' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 80],
'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
],
];
protected $webserviceParameters = [
'fields' => [
'id_zone' => ['xlink_resource' => 'zones'],
'id_country' => ['xlink_resource' => 'countries'],
],
];
public static function getStates($idLang = false, $active = false)
{
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
SELECT `id_state`, `id_country`, `id_zone`, `iso_code`, `name`, `active`
FROM `' . _DB_PREFIX_ . 'state`
' . ($active ? 'WHERE active = 1' : '') . '
ORDER BY `name` ASC');
}
/**
* Get a state name with its ID.
*
* @param int $idState Country ID
*
* @return bool|string State name
*/
public static function getNameById($idState)
{
if (!$idState) {
return false;
}
$cacheId = 'State::getNameById_' . (int) $idState;
if (!Cache::isStored($cacheId)) {
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
'
SELECT `name`
FROM `' . _DB_PREFIX_ . 'state`
WHERE `id_state` = ' . (int) $idState
);
Cache::store($cacheId, $result);
return $result;
}
return Cache::retrieve($cacheId);
}
/**
* Get State ID with its name.
*
* @param string $state State ID
*
* @return bool|int state id
*/
public static function getIdByName($state)
{
if (empty($state)) {
return false;
}
$cacheId = 'State::getIdByName_' . pSQL($state);
if (!Cache::isStored($cacheId)) {
$result = (int) Db::getInstance()->getValue('
SELECT `id_state`
FROM `' . _DB_PREFIX_ . 'state`
WHERE `name` = \'' . pSQL($state) . '\'
');
Cache::store($cacheId, $result);
return $result;
}
return Cache::retrieve($cacheId);
}
/**
* Get a state id with its iso code.
*
* @param string $isoCode Iso code
* @param int|null $idCountry
*
* @return int state id
*/
public static function getIdByIso($isoCode, $idCountry = null)
{
return (int) Db::getInstance()->getValue(
'SELECT `id_state`
FROM `' . _DB_PREFIX_ . 'state`
WHERE `iso_code` = \'' . pSQL($isoCode) . '\'
' . ($idCountry ? 'AND `id_country` = ' . (int) $idCountry : '')
);
}
/**
* Delete a state only if is not in use.
*
* @return bool
*/
public function delete()
{
if (!$this->isUsed()) {
// Database deletion
$result = Db::getInstance()->delete($this->def['table'], '`' . $this->def['primary'] . '` = ' . (int) $this->id);
if (!$result) {
return false;
}
// Database deletion for multilingual fields related to the object
if (!empty($this->def['multilang'])) {
Db::getInstance()->delete(bqSQL($this->def['table']) . '_lang', '`' . $this->def['primary'] . '` = ' . (int) $this->id);
}
return $result;
} else {
return false;
}
}
/**
* Check if a state is used.
*
* @return bool
*/
public function isUsed()
{
return $this->countUsed() > 0;
}
/**
* Returns the number of utilisation of a state.
*
* @return int count for this state
*/
public function countUsed()
{
return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
'SELECT COUNT(*)
FROM `' . _DB_PREFIX_ . 'address`
WHERE `' . $this->def['primary'] . '` = ' . (int) $this->id
);
}
/**
* Get states by Country ID.
*
* @param int $idCountry Country ID
* @param bool $active true if the state must be active
* @param string $orderBy order by field
* @param string $sort sort key (ASC or DESC)
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public static function getStatesByIdCountry($idCountry, $active = false, $orderBy = null, $sort = 'ASC')
{
if (empty($idCountry)) {
throw new PrestaShopException('Country ID is invalid.');
}
$available_sort = ['DESC', 'ASC', 'asc', 'desc'];
$sql = new DbQuery();
$sql->select('*');
$sql->from('state', 's');
$sql->where('s.id_country = ' . (int) $idCountry . ($active ? ' AND s.active = 1' : ''));
if (array_key_exists($orderBy, static::$definition['fields'])) {
$sort = trim($sort);
if (in_array($sort, $available_sort)) {
$orderBy = $orderBy . ' ' . $sort;
}
$sql->orderBy($orderBy);
}
return Db::getInstance()->executeS($sql);
}
/**
* Get Zone ID.
*
* @param int $idState State ID
*
* @return false|string|null
*/
public static function getIdZone($idState)
{
if (!Validate::isUnsignedId($idState)) {
throw new PrestaShopException('State ID is invalid.');
}
return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
'
SELECT `id_zone`
FROM `' . _DB_PREFIX_ . 'state`
WHERE `id_state` = ' . (int) $idState
);
}
/**
* @param array $idsStates State IDs
* @param int $idZone Zone ID
*
* @return bool
*/
public function affectZoneToSelection($idsStates, $idZone)
{
// cast every array values to int (security)
$idsStates = array_map('intval', $idsStates);
return Db::getInstance()->execute('
UPDATE `' . _DB_PREFIX_ . 'state` SET `id_zone` = ' . (int) $idZone . ' WHERE `id_state` IN (' . implode(',', $idsStates) . ')
');
}
}

193
classes/Store.php Normal file
View File

@@ -0,0 +1,193 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShopBundle\Form\Admin\Type\FormattedTextareaType;
/**
* Class StoreCore.
*/
class StoreCore extends ObjectModel
{
/** @var int Store id */
public $id;
/** @var int|bool Store id */
public $id_image;
/** @var int Country id */
public $id_country;
/** @var int State id */
public $id_state;
/** @var string|array<string> Name */
public $name;
/** @var string|array<string> Address first line */
public $address1;
/** @var string|array<string> Address second line (optional) */
public $address2;
/** @var string Postal code */
public $postcode;
/** @var string City */
public $city;
/** @var float Latitude */
public $latitude;
/** @var float Longitude */
public $longitude;
/** @var string|array Store hours (PHP serialized) */
public $hours;
/** @var string Phone number */
public $phone;
/** @var string Fax number */
public $fax;
/** @var string|array<string> Note */
public $note;
/** @var string e-mail */
public $email;
/** @var string Object creation date */
public $date_add;
/** @var string Object last modification date */
public $date_upd;
/** @var bool Store status */
public $active = true;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'store',
'primary' => 'id_store',
'multilang' => true,
'fields' => [
'id_country' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'id_state' => ['type' => self::TYPE_INT, 'validate' => 'isNullOrUnsignedId'],
'postcode' => ['type' => self::TYPE_STRING, 'size' => 12],
'city' => ['type' => self::TYPE_STRING, 'validate' => 'isCityName', 'required' => true, 'size' => 64],
'latitude' => ['type' => self::TYPE_FLOAT, 'validate' => 'isCoordinate', 'size' => 13],
'longitude' => ['type' => self::TYPE_FLOAT, 'validate' => 'isCoordinate', 'size' => 13],
'phone' => ['type' => self::TYPE_STRING, 'validate' => 'isPhoneNumber', 'size' => 16],
'fax' => ['type' => self::TYPE_STRING, 'validate' => 'isPhoneNumber', 'size' => 16],
'email' => ['type' => self::TYPE_STRING, 'validate' => 'isEmail', 'size' => 255],
'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'required' => true],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
/* Lang fields */
'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 255],
'address1' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isAddress', 'required' => true, 'size' => 255],
'address2' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isAddress', 'size' => 255],
'hours' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isJson', 'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4],
'note' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isCleanHtml', 'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4],
],
];
protected $webserviceParameters = [
'fields' => [
'id_country' => ['xlink_resource' => 'countries'],
'id_state' => ['xlink_resource' => 'states'],
'hours' => ['getter' => 'getWsHours', 'setter' => 'setWsHours'],
],
];
/**
* StoreCore constructor.
*
* @param int|null $idStore
* @param int|null $idLang
*/
public function __construct($idStore = null, $idLang = null)
{
parent::__construct($idStore, $idLang);
$this->id_image = ($this->id && file_exists(_PS_STORE_IMG_DIR_ . (int) $this->id . '.jpg')) ? (int) $this->id : false;
$this->image_dir = _PS_STORE_IMG_DIR_;
}
/**
* Get Stores by language.
*
* @param int $idLang
*
* @return array
*/
public static function getStores($idLang)
{
return Db::getInstance()->executeS(
'SELECT s.`id_store` AS `id`, s.*, sl.*
FROM `' . _DB_PREFIX_ . 'store` s ' . Shop::addSqlAssociation('store', 's') . '
LEFT JOIN `' . _DB_PREFIX_ . 'store_lang` sl ON (sl.`id_store` = s.`id_store` AND sl.`id_lang` = ' . (int) $idLang . ')
WHERE s.`active` = 1
ORDER BY sl.`name` ASC'
);
}
/**
* Get hours for webservice.
*
* @return string
*/
public function getWsHours()
{
return $this->hours;
}
/**
* Set hours for webservice.
*
* @param string $hours
*
* @return bool
*/
public function setWsHours($hours)
{
if (!is_string($hours)) {
return false;
}
$this->hours = $hours;
return true;
}
/**
* This method is allow to know if a store exists for AdminImportController.
*
* @return bool
*/
public static function storeExists($idStore)
{
return (bool) Db::getInstance()->getValue(
'
SELECT `id_store`
FROM `' . _DB_PREFIX_ . 'store` a
WHERE a.`id_store` = ' . (int) $idStore,
false
);
}
/**
* This method checks if at least one store is configured
*
* @return bool
*/
public static function atLeastOneStoreExists()
{
return (bool) Db::getInstance()->getValue('SELECT `id_store` FROM `' . _DB_PREFIX_ . 'store`', false);
}
}

481
classes/Supplier.php Normal file
View File

@@ -0,0 +1,481 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
use PrestaShopBundle\Form\Admin\Type\FormattedTextareaType;
/**
* Class SupplierCore.
*/
class SupplierCore extends ObjectModel
{
public $id;
/** @var int supplier ID */
public $id_supplier;
/** @var string Name */
public $name;
/** @var string|array<int, string> A short description for the discount */
public $description;
/** @var string Object creation date */
public $date_add;
/** @var string Object last modification date */
public $date_upd;
/** @var string Friendly URL */
public $link_rewrite;
/** @var string|array<int, string> Meta title */
public $meta_title;
/** @var string|array<int, string> Meta description */
public $meta_description;
/** @var bool active */
public $active;
/**
* @see ObjectModel::$definition
*/
public static $definition = [
'table' => 'supplier',
'primary' => 'id_supplier',
'multilang' => true,
'fields' => [
'name' => ['type' => self::TYPE_STRING, 'validate' => 'isCatalogName', 'required' => true, 'size' => 64],
'active' => ['type' => self::TYPE_BOOL],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
/* Lang fields */
'description' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml', 'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4],
'meta_title' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
'meta_description' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 512],
],
];
protected $webserviceParameters = [
'fields' => [
'link_rewrite' => ['sqlId' => 'link_rewrite'],
],
];
/**
* SupplierCore constructor.
*
* @param int|null $id
* @param int|null $idLang
*/
public function __construct($id = null, $idLang = null)
{
parent::__construct($id, $idLang);
$this->link_rewrite = $this->getLink();
$this->image_dir = _PS_SUPP_IMG_DIR_;
}
public function getLink()
{
return Tools::str2url($this->name);
}
/**
* Return suppliers.
*
* @return bool|array Suppliers
*/
public static function getSuppliers($getNbProducts = false, $idLang = 0, $active = true, $p = false, $n = false, $allGroups = false, $withProduct = false)
{
if (!$idLang) {
$idLang = Configuration::get('PS_LANG_DEFAULT');
}
if (!Group::isFeatureActive()) {
$allGroups = true;
}
$query = new DbQuery();
$query->select('s.*, sl.`description`');
$query->from('supplier', 's');
$query->leftJoin('supplier_lang', 'sl', 's.`id_supplier` = sl.`id_supplier` AND sl.`id_lang` = ' . (int) $idLang);
$query->join(Shop::addSqlAssociation('supplier', 's'));
if ($active) {
$query->where('s.`active` = 1');
}
if ($withProduct) {
$query->where('s.`id_supplier` IN (SELECT `id_supplier` FROM `' . _DB_PREFIX_ . 'product_supplier`)');
}
$query->orderBy(' s.`name` ASC');
$query->limit($n, ($p - 1) * $n);
$query->groupBy('s.id_supplier');
$suppliers = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query);
if ($suppliers === false) {
return false;
}
if ($getNbProducts) {
$sqlGroups = '';
if (!$allGroups) {
$groups = FrontController::getCurrentCustomerGroups();
$sqlGroups = (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id);
}
$results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
'
SELECT ps.`id_supplier`, COUNT(DISTINCT ps.`id_product`) as nb_products
FROM `' . _DB_PREFIX_ . 'product_supplier` ps
JOIN `' . _DB_PREFIX_ . 'product` p ON (ps.`id_product`= p.`id_product`)
' . Shop::addSqlAssociation('product', 'p') . '
LEFT JOIN `' . _DB_PREFIX_ . 'supplier` as m ON (m.`id_supplier`= p.`id_supplier`)
WHERE product_shop.`visibility` NOT IN ("none")' .
($active ? ' AND product_shop.`active` = 1' : '') .
($allGroups ? '' : '
AND ps.`id_product` IN (
SELECT cp.`id_product`
FROM `' . _DB_PREFIX_ . 'category_group` cg
LEFT JOIN `' . _DB_PREFIX_ . 'category_product` cp ON (cp.`id_category` = cg.`id_category`)
WHERE cg.`id_group` ' . $sqlGroups . '
)') . '
GROUP BY ps.`id_supplier`'
);
$counts = [];
foreach ($results as $result) {
$counts[(int) $result['id_supplier']] = (int) $result['nb_products'];
}
foreach ($suppliers as $key => $supplier) {
if (array_key_exists((int) $supplier['id_supplier'], $counts)) {
$suppliers[$key]['nb_products'] = $counts[(int) $supplier['id_supplier']];
} else {
$suppliers[$key]['nb_products'] = 0;
}
}
}
$nbSuppliers = count($suppliers);
$rewriteSettings = (int) Configuration::get('PS_REWRITING_SETTINGS');
for ($i = 0; $i < $nbSuppliers; ++$i) {
$suppliers[$i]['link_rewrite'] = ($rewriteSettings ? Tools::str2url($suppliers[$i]['name']) : 0);
}
return $suppliers;
}
/**
* List of suppliers.
*
* @param int $idLang Specify the id of the language used
* @param string $format
*
* @return array Suppliers lite tree
*/
public static function getLiteSuppliersList($idLang = null, $format = 'default')
{
$idLang = null === $idLang ? Context::getContext()->language->id : (int) $idLang;
$suppliersList = [];
$suppliers = Supplier::getSuppliers(false, $idLang, true);
if ($suppliers && count($suppliers)) {
foreach ($suppliers as $supplier) {
if ($format === 'sitemap') {
$suppliersList[] = [
'id' => 'supplier-page-' . (int) $supplier['id_supplier'],
'label' => $supplier['name'],
'url' => Context::getContext()->link->getSupplierLink($supplier['id_supplier'], $supplier['link_rewrite']),
'children' => [],
];
} else {
$suppliersList[] = [
'id' => (int) $supplier['id_supplier'],
'link' => Context::getContext()->link->getSupplierLink($supplier['id_supplier'], $supplier['link_rewrite']),
'name' => $supplier['name'],
'desc' => $supplier['description'],
'children' => [],
];
}
}
}
return $suppliersList;
}
/**
* Return name from id.
*
* @param int $id_supplier Supplier ID
*
* @return string name
*/
protected static $cache_name = [];
public static function getNameById($idSupplier)
{
if (!isset(self::$cache_name[$idSupplier])) {
self::$cache_name[$idSupplier] = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
SELECT `name` FROM `' . _DB_PREFIX_ . 'supplier` WHERE `id_supplier` = ' . (int) $idSupplier);
}
return self::$cache_name[$idSupplier];
}
public static function getIdByName($name)
{
$result = Db::getInstance()->getRow('
SELECT `id_supplier`
FROM `' . _DB_PREFIX_ . 'supplier`
WHERE `name` = \'' . pSQL($name) . '\'');
if (isset($result['id_supplier'])) {
return (int) $result['id_supplier'];
}
return false;
}
/**
* @param int $idSupplier
* @param int $idLang
* @param int $p
* @param int $n
* @param string|null $orderBy
* @param string|null $orderWay
* @param bool $getTotal
* @param bool $active
* @param bool $activeCategory
*
* @return int|array|bool
*/
public static function getProducts(
$idSupplier,
$idLang,
$p,
$n,
$orderBy = null,
$orderWay = null,
$getTotal = false,
$active = true,
$activeCategory = true
) {
$context = Context::getContext();
$front = true;
if (!in_array($context->controller->controller_type, ['front', 'modulefront'])) {
$front = false;
}
if ($p < 1) {
$p = 1;
}
if (empty($orderBy) || $orderBy == 'position') {
$orderBy = 'name';
}
if (empty($orderWay)) {
$orderWay = 'ASC';
}
if (!Validate::isOrderBy($orderBy) || !Validate::isOrderWay($orderWay)) {
throw new PrestaShopException('Invalid sorting parameters provided.');
}
$sqlGroups = '';
$groups = [];
if (Group::isFeatureActive()) {
$groups = FrontController::getCurrentCustomerGroups();
$sqlGroups = 'WHERE cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id);
}
/* Return only the number of products */
if ($getTotal) {
return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
SELECT COUNT(DISTINCT ps.`id_product`)
FROM `' . _DB_PREFIX_ . 'product_supplier` ps
JOIN `' . _DB_PREFIX_ . 'product` p ON (ps.`id_product`= p.`id_product`)
' . Shop::addSqlAssociation('product', 'p') . '
WHERE ps.`id_supplier` = ' . (int) $idSupplier . '
' . ($active ? ' AND product_shop.`active` = 1' : '') . '
' . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . '
AND p.`id_product` IN (
SELECT cp.`id_product`
FROM `' . _DB_PREFIX_ . 'category_product` cp
' . (Group::isFeatureActive() ? 'LEFT JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cp.`id_category` = cg.`id_category`)' : '') . '
' . ($activeCategory ? ' INNER JOIN `' . _DB_PREFIX_ . 'category` ca ON cp.`id_category` = ca.`id_category` AND ca.`active` = 1' : '') . '
' . $sqlGroups . '
)');
}
$nbDaysNewProduct = Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20;
if (strpos('.', $orderBy) > 0) {
$orderBy = explode('.', $orderBy);
$orderBy = pSQL($orderBy[0]) . '.`' . pSQL($orderBy[1]) . '`';
}
$alias = '';
if (in_array($orderBy, ['price', 'date_add', 'date_upd'])) {
$alias = 'product_shop.';
} elseif ($orderBy == 'id_product') {
$alias = 'p.';
} elseif ($orderBy == 'manufacturer_name') {
$orderBy = 'name';
$alias = 'm.';
}
$sql = 'SELECT p.*, product_shop.*, stock.out_of_stock,
IFNULL(stock.quantity, 0) as quantity,
pl.`description`,
pl.`description_short`,
pl.`link_rewrite`,
pl.`meta_description`,
pl.`meta_title`,
pl.`name`,
image_shop.`id_image` id_image,
il.`legend`,
s.`name` AS supplier_name,
DATEDIFF(p.`date_add`, DATE_SUB("' . date('Y-m-d') . ' 00:00:00", INTERVAL ' . $nbDaysNewProduct . ' DAY)) > 0 AS new,
m.`name` AS manufacturer_name' . (Combination::isFeatureActive() ? ', product_attribute_shop.minimal_quantity AS product_attribute_minimal_quantity, IFNULL(product_attribute_shop.id_product_attribute,0) id_product_attribute' : '') . '
FROM `' . _DB_PREFIX_ . 'product` p
' . Shop::addSqlAssociation('product', 'p') . '
JOIN `' . _DB_PREFIX_ . 'product_supplier` ps ON (ps.id_product = p.id_product) ' .
(Combination::isFeatureActive() ? 'LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` product_attribute_shop
ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shop.id_shop=' . (int) $context->shop->id . ')' : '') . '
LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (p.`id_product` = pl.`id_product`
AND pl.`id_lang` = ' . (int) $idLang . Shop::addSqlRestrictionOnLang('pl') . ')
LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
ON (image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop=' . (int) $context->shop->id . ')
LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`id_image` = il.`id_image`
AND il.`id_lang` = ' . (int) $idLang . ')
LEFT JOIN `' . _DB_PREFIX_ . 'supplier` s ON s.`id_supplier` = p.`id_supplier`
LEFT JOIN `' . _DB_PREFIX_ . 'manufacturer` m ON m.`id_manufacturer` = p.`id_manufacturer`
' . Product::sqlStock('p', 0);
if (Group::isFeatureActive() || $activeCategory) {
$sql .= 'JOIN `' . _DB_PREFIX_ . 'category_product` cp ON (p.id_product = cp.id_product)';
if (Group::isFeatureActive()) {
$sql .= 'JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cp.`id_category` = cg.`id_category` AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '= 1') . ')';
}
if ($activeCategory) {
$sql .= 'JOIN `' . _DB_PREFIX_ . 'category` ca ON cp.`id_category` = ca.`id_category` AND ca.`active` = 1';
}
}
$sql .= '
WHERE ps.`id_supplier` = ' . (int) $idSupplier . '
' . ($active ? ' AND product_shop.`active` = 1' : '') . '
' . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . '
GROUP BY ps.id_product';
if ($orderBy !== 'price') {
$sql .= '
ORDER BY ' . $alias . '`' . pSQL($orderBy) . '` ' . pSQL($orderWay) . '
LIMIT ' . (((int) $p - 1) * (int) $n) . ',' . (int) $n;
}
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql, true, false);
if (!$result) {
return false;
}
if ($orderBy === 'price') {
Tools::orderbyPrice($result, $orderWay);
$result = array_slice($result, (int) (($p - 1) * $n), (int) $n);
}
return $result;
}
/**
* Get Products of this supplier (lite).
*
* @param int $idLang Language ID
*
* @return array|false|mysqli_result|PDOStatement|resource|null
*/
public function getProductsLite($idLang)
{
$context = Context::getContext();
$front = true;
if (!in_array($context->controller->controller_type, ['front', 'modulefront'])) {
$front = false;
}
$sql = '
SELECT p.`id_product`,
pl.`name`
FROM `' . _DB_PREFIX_ . 'product` p
' . Shop::addSqlAssociation('product', 'p') . '
LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (
p.`id_product` = pl.`id_product`
AND pl.`id_lang` = ' . (int) $idLang . '
)
INNER JOIN `' . _DB_PREFIX_ . 'product_supplier` ps ON (
ps.`id_product` = p.`id_product`
AND ps.`id_supplier` = ' . (int) $this->id . '
)
' . ($front ? ' WHERE product_shop.`visibility` IN ("both", "catalog")' : '') . '
GROUP BY p.`id_product`';
$res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
return $res;
}
/**
* Tells if a supplier exists.
*
* @param int $idSupplier Supplier id
*
* @return bool
*/
public static function supplierExists($idSupplier)
{
$query = new DbQuery();
$query->select('id_supplier');
$query->from('supplier');
$query->where('id_supplier = ' . (int) $idSupplier);
$res = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query, false);
return $res > 0;
}
/**
* @see ObjectModel::delete()
*/
public function delete()
{
$res = parent::delete();
if ($res) {
CartRule::cleanProductRuleIntegrity('suppliers', $this->id);
return $this->deleteImage();
}
return $res;
}
/**
* Gets product informations.
*
* @param int $idSupplier
* @param int $idProduct
* @param int $idProductAttribute
*
* @return array
*/
public static function getProductInformationsBySupplier($idSupplier, $idProduct, $idProductAttribute = 0)
{
$query = new DbQuery();
$query->select('product_supplier_reference, product_supplier_price_te, id_currency');
$query->from('product_supplier');
$query->where('id_supplier = ' . (int) $idSupplier);
$query->where('id_product = ' . (int) $idProduct);
$query->where('id_product_attribute = ' . (int) $idProductAttribute);
$res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query);
return count($res) ? $res[0] : [];
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
/**
* Class SupplierAddressCore.
*
* Holds address info of a Supplier.
* This class extends AddressCore to be differentiated from other AddressCore objects in DB.
*/
class SupplierAddressCore extends AddressCore
{
}

Some files were not shown because too many files have changed in this diff Show More