* @link http://www.oxidmodule.com */ namespace D3\GeoIp\Application\Model; use Assert\Assert; use Assert\InvalidArgumentException; use D3\ModCfg\Application\Model\Configuration\d3_cfg_mod; use D3\ModCfg\Application\Model\d3str; use D3\ModCfg\Application\Model\Exception\d3_cfg_mod_exception; use D3\ModCfg\Application\Model\Exception\d3ShopCompatibilityAdapterException; use D3\ModCfg\Application\Model\Log\d3log; use Doctrine\DBAL\DBALException; use OxidEsales\Eshop\Application\Model\Country; use OxidEsales\Eshop\Core\Config; use OxidEsales\Eshop\Core\DatabaseProvider; use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; use OxidEsales\Eshop\Core\Exception\StandardException; use OxidEsales\Eshop\Core\Model\BaseModel; use OxidEsales\Eshop\Core\Registry; class d3geoip extends BaseModel { const SKIPURL_REQUEST_PARAM = 'forceUrl'; const SKIPURL_SESSION_PARAM = 'd3geoipForceUrl'; protected $_sClassName = 'd3geoip'; private $_sModId = 'd3_geoip'; public $oCountry; public $oD3Log; /** * Class constructor, initiates parent constructor (parent::oxI18n()). */ public function __construct() { parent::__construct(); $this->init('d3geoip'); } /** * get oxcountry object by given IP address (optional) * * @param string|null $sIP optional * * @return Country * @throws d3ShopCompatibilityAdapterException * @throws d3_cfg_mod_exception * @throws DBALException * @throws DatabaseConnectionException * @throws DatabaseErrorException * @throws StandardException */ public function getUserLocationCountryObject(string $sIP = null) { if (!$this->oCountry) { startProfile(__METHOD__); if (!$sIP) { $sIP = $this->getIP(); } $sISOAlpha = $this->loadByIP(Registry::getConfig()->checkParamSpecialChars($sIP)); if (!$sISOAlpha) { $this->_getLog()->log( d3log::ERROR, __CLASS__, __FUNCTION__, __LINE__, 'get ISO by IP failed', $sIP ); $this->oCountry = $this->getCountryFallBackObject(); } else { $this->_getLog()->log( d3log::INFO, __CLASS__, __FUNCTION__, __LINE__, 'get ISO by IP', $sIP." => ".$sISOAlpha ); $this->oCountry = $this->getCountryObject($sISOAlpha); } stopProfile(__METHOD__); } return $this->oCountry; } /** * get IP address from client or set test IP address * * @return string * @throws DBALException * @throws DatabaseConnectionException * @throws DatabaseErrorException */ public function getIP() { startProfile(__METHOD__); if ($this->_getModConfig()->getValue('blUseTestIp') && $this->_getModConfig()->getValue('sTestIp') ) { $sIP = $this->_getModConfig()->getValue('sTestIp'); } elseif ($this->_getModConfig()->getValue('blUseTestCountry') && $this->_getModConfig()->getValue('sTestCountryIp') ) { $sIP = $this->_getModConfig()->getValue('sTestCountryIp'); } else { $sIP = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['HTTP_CLIENT_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_FORWARDED'] ?? $_SERVER['HTTP_FORWARDED_FOR'] ?? $_SERVER['HTTP_FORWARDED'] ?? $_SERVER['REMOTE_ADDR'] ?? 'UNKNOWN'; } $sIP = str_replace(' ', '', $sIP); stopProfile(__METHOD__); return Registry::getConfig()->checkParamSpecialChars($sIP); } /** * get ISO alpha 2 ID by IP address * * @param string $sIP IP address * * @return string * @throws DatabaseConnectionException */ public function loadByIP(string $sIP) { startProfile(__METHOD__); $sSelect = " SELECT d3iso FROM ".$this->_sClassName." WHERE LPAD( BINARY( if( IS_IPV4('" . $sIP . "'), INET_ATON('" . $sIP . "'), INET6_ATON('" . $sIP . "') ) ), 16, 0 ) BETWEEN D3STARTIPBIN AND D3ENDIPBIN"; $oDB = DatabaseProvider::getDb(DatabaseProvider::FETCH_MODE_ASSOC); $sISO = $oDB->getOne($sSelect); stopProfile(__METHOD__); return $sISO; } /** * get Country object by ISO alpha 2 ID * * @param string $sISOAlpha * * @return Country * @throws DatabaseConnectionException */ public function getCountryObject(string $sISOAlpha) { startProfile(__METHOD__); /** @var Country $oCountry */ $oCountry = oxNew(Country::class); $sSelect = "SELECT oxid FROM ".$oCountry->getViewName(). " WHERE OXISOALPHA2 = '".$sISOAlpha."' AND OXACTIVE = '1'"; $oDB = DatabaseProvider::getDb(); $oCountry->load($oDB->getOne($sSelect)); stopProfile(__METHOD__); return $oCountry; } /** * get Country object for fallback, if set * * @return Country * @throws DBALException * @throws DatabaseConnectionException * @throws DatabaseErrorException */ public function getCountryFallBackObject() { startProfile(__METHOD__); $oCountry = oxNew('oxcountry'); if ($this->_getModConfig()->getValue('blUseFallback') && $this->_getModConfig()->getValue('sFallbackCountryId') ) { $oCountry->load($this->_getModConfig()->getValue('sFallbackCountryId')); } stopProfile(__METHOD__); return $oCountry; } /** * check module active state and set user country specific language * * @throws DBALException * @throws DatabaseConnectionException * @throws DatabaseErrorException * @throws StandardException * @throws d3ShopCompatibilityAdapterException * @throws d3_cfg_mod_exception */ public function setCountryLanguage() { startProfile(__METHOD__); $this->_getModConfig()->d3getLog()->info(__CLASS__, __FUNCTION__, __LINE__, 'start shop or url switch'); $this->performURLSwitch(); $this->performShopSwitch(); $this->_getModConfig()->d3getLog()->info(__CLASS__, __FUNCTION__, __LINE__, 'end shop or url switch'); if (!$this->_getModConfig()->isActive() || false == $this->_getModConfig()->getValue('blChangeLang') ) { $this->_getModConfig()->d3getLog()->info(__CLASS__, __FUNCTION__, __LINE__, 'language change option or module is disabled'); stopProfile(__METHOD__); return; } $oCountry = $this->getUserLocationCountryObject(); try { $this->canChangeLanguage( $oCountry ); $language = (int) $oCountry->getFieldData('d3geoiplang'); $this->_getModConfig()->d3getLog()->info( __CLASS__, __FUNCTION__, __LINE__, 'set language', $this->getIP().' => '.$language ); Registry::getLang()->setTplLanguage($language); Registry::getLang()->setBaseLanguage($language); Registry::getSession()->setVariable('d3isSetLang', true); Registry::getLogger()->debug('language set', [$language]); } catch (InvalidArgumentException $e) { Registry::getLogger()->debug($e->getMessage(), [$oCountry]); } stopProfile(__METHOD__); } /** * check module active state and set user country specific currency * * @throws DBALException * @throws DatabaseConnectionException * @throws DatabaseErrorException * @throws StandardException * @throws d3ShopCompatibilityAdapterException * @throws d3_cfg_mod_exception */ public function setCountryCurrency() { if (!$this->_getModConfig()->isActive() || false == $this->_getModConfig()->getValue('blChangeCurr') ) { return; } startProfile(__METHOD__); $oCountry = $this->getUserLocationCountryObject(); try { $this->canChangeCurrency( $oCountry ); $this->_getLog()->log( d3log::INFO, __CLASS__, __FUNCTION__, __LINE__, 'set currency', $this->getIP().' => '.$oCountry->getFieldData('d3geoipcur') ); Registry::getConfig()->setActShopCurrency((int) $oCountry->getFieldData('d3geoipcur')); Registry::getSession()->setVariable('d3isSetCurr', true); } catch (InvalidArgumentException $e) { Registry::getLogger()->debug($e->getMessage(), [$oCountry]); } stopProfile(__METHOD__); } /** * @param $oCurr * * @return bool * @throws DBALException * @throws DatabaseConnectionException * @throws DatabaseErrorException * @throws StandardException * @throws d3ShopCompatibilityAdapterException * @throws d3_cfg_mod_exception */ public function hasNotSetCurrency($oCurr) { $oCountry = $this->getUserLocationCountryObject(); if ($oCountry->getFieldData('d3geoipcur') > 0 && $oCurr->id != $oCountry->getFieldData('d3geoipcur') ) { return true; } return false; } /** * check module active state and perform switching to user country specific shop (EE only) * * @throws DBALException * @throws DatabaseConnectionException * @throws DatabaseErrorException * @throws StandardException * @throws d3ShopCompatibilityAdapterException * @throws d3_cfg_mod_exception */ public function performShopSwitch() { if (!$this->_getModConfig()->isActive() || !$this->_getModConfig()->getValue('blChangeShop')) { $this->_getModConfig()->d3getLog()->info(__CLASS__, __FUNCTION__, __LINE__, 'shop change option or module is disabled'); return; } startProfile(__METHOD__); $oCountry = $this->getUserLocationCountryObject(); $iNewShop = $oCountry->getFieldData('d3geoipshop'); $this->_getModConfig()->d3getLog()->info(__CLASS__, __FUNCTION__, __LINE__, 'check allowed shop change'); try { $this->canChangeShop( $oCountry, $iNewShop ); $this->_getModConfig()->d3getLog()->info(__CLASS__, __FUNCTION__, __LINE__, 'prepare shop change to '.$iNewShop); $oNewConf = new Config(); $oNewConf->setShopId($iNewShop); $oNewConf->init(); Registry::getConfig()->onShopChange(); if (!Registry::getSession()->getVariable('d3isSetLang') && $this->_getModConfig()->getValue('blChangeLang') && $oCountry->getFieldData('d3geoiplang') > -1 ) { $sLangId = $oCountry->getFieldData('d3geoiplang'); } else { $sLangId = ''; } /** @var $oStr d3str */ $oStr = Registry::get(d3str::class); $aParams = array( 'd3redirect' => '1', 'fnc' => Registry::getRequest()->getRequestEscapedParameter('fnc'), 'shp' => $iNewShop ); $sUrl = str_replace( '&', '&', $oStr->generateParameterUrl($oNewConf->getShopHomeUrl($sLangId), $aParams) ); $this->_getLog()->log( d3log::INFO, __CLASS__, __FUNCTION__, __LINE__, 'change shop', $this->getIP().' => '.$sUrl ); $this->_getModConfig()->d3getLog()->info(__CLASS__, __FUNCTION__, __LINE__, 'change to shop url', $sUrl); header("Location: ".$sUrl); exit(); } catch (InvalidArgumentException $e) { Registry::getLogger()->debug($e->getMessage(), [$oCountry]); } stopProfile(__METHOD__); } /** * check module active state and perform switching to user country specific url * * @throws DBALException * @throws DatabaseConnectionException * @throws DatabaseErrorException * @throws StandardException * @throws d3ShopCompatibilityAdapterException * @throws d3_cfg_mod_exception */ public function performURLSwitch() { if (!$this->_getModConfig()->isActive() || false == $this->_getModConfig()->getValue('blChangeURL')) { return; } startProfile(__METHOD__); if (Registry::getRequest()->getRequestEscapedParameter(self::SKIPURL_REQUEST_PARAM)) { Registry::getSession()->setVariable(self::SKIPURL_SESSION_PARAM, true); } $oCountry = $this->getUserLocationCountryObject(); try { $this->canRedirect( $oCountry ); $sNewUrl = $oCountry->getFieldData('d3geoipurl'); $this->_getLog()->log( d3log::INFO, __CLASS__, __FUNCTION__, __LINE__, 'change url', $this->getIP().' => '.$oCountry->getFieldData('d3geoipurl') ); header("Location: ".$sNewUrl); exit(); } catch (InvalidArgumentException $e) { Registry::getLogger()->debug($e->getMessage(), [$oCountry]); } stopProfile(__METHOD__); } /** * @return bool */ protected function dontSkipUrlRedirect() { return false === ( Registry::getRequest()->getRequestEscapedParameter(self::SKIPURL_REQUEST_PARAM) || Registry::getSession()->getVariable(self::SKIPURL_SESSION_PARAM) ); } /** * get all shop urls * * @return array */ public function getShopUrls() { startProfile(__METHOD__); $oShoplist = oxNew('oxshoplist'); $oShoplist->getList(); $aShopUrls = array(); foreach ($oShoplist->arrayKeys() as $sId) { $aShopUrls[$sId] = Registry::getConfig()->getShopConfVar('sMallShopURL', $sId); } stopProfile(__METHOD__); return $aShopUrls; } /** * get modcfg instance * * @return d3_cfg_mod * @throws DBALException * @throws DatabaseConnectionException * @throws DatabaseErrorException */ protected function _getModConfig() { return d3_cfg_mod::get($this->_sModId); } /** * get d3log instance * * @return d3log * @throws DBALException * @throws DatabaseConnectionException * @throws DatabaseErrorException */ protected function _getLog() { if (!$this->oD3Log) { $this->oD3Log = $this->_getModConfig()->d3getLog(); } return $this->oD3Log; } /** * @param Country $oCountry * * @throws InvalidArgumentException */ protected function canRedirect( Country $oCountry ) { Assert::that($this->dontSkipUrlRedirect())->true('redirect skip is not provided'); $this->isUserInFrontend(); Assert::that($oCountry->getId())->notBlank('no country loaded'); Assert::that($oCountry->getFieldData( 'd3geoipurl' ))->notBlank('no redirect url set'); } /** * @param Country $oCountry * * @return bool */ /** * @param Country $oCountry * * @throws InvalidArgumentException */ protected function canChangeLanguage( Country $oCountry ) { $this->isUserInFrontend(); Assert::that(Registry::getSession()->getVariable( 'd3isSetLang' ))->null('language already set'); Assert::that($oCountry->getId())->notBlank('no country loaded'); Assert::that($oCountry->getFieldData( 'd3geoiplang' ))->greaterThan(-1, 'no target language set'); } /** * @param Country $oCountry * * @throws InvalidArgumentException */ protected function canChangeCurrency( Country $oCountry ) { $this->isUserInFrontend(); Assert::that(Registry::getSession()->getVariable( 'd3isSetCurr' ))->null('currency already set'); Assert::that($oCountry->getId())->notBlank('no country loaded'); Assert::that($oCountry->getFieldData( 'd3geoipcur' ))->greaterThan(-1, 'no target currency set'); } /** * @param Country $oCountry * @param $iNewShop * * @throws InvalidArgumentException */ protected function canChangeShop( Country $oCountry, $iNewShop ) { Assert::that(Registry::getRequest()->getRequestEscapedParameter( 'd3redirect' ))->notSame('1', 'redirect already performed'); $this->isUserInFrontend(); Assert::that($oCountry->getId())->notBlank('no country loaded'); Assert::that(Registry::getConfig()->isMall())->true('no mall installation'); Assert::that($iNewShop)->greaterThan(-1, 'no target shop id set'); Assert::that( $iNewShop != Registry::getConfig()->getShopId() || strtolower( Registry::getConfig()->getActiveView()->getClassKey() ) == 'mallstart' )->true('no different shop and no mallstart'); } /** * @throws InvalidArgumentException */ protected function isUserInFrontend() { Assert::that($this->isAdmin())->false('User is in admin'); Assert::that(Registry::getUtils()->isSearchEngine())->false('User is search engine'); } }