├── .eslintignore ├── image1.png ├── plugin.png ├── .githooks ├── install_hooks.sh └── pre-commit ├── composer.json ├── Resources ├── views │ ├── frontend │ │ ├── index │ │ │ └── index.tpl │ │ └── _public │ │ │ └── src │ │ │ └── js │ │ │ └── jquery.redirect.js │ └── widgets │ │ └── swag_browser_language │ │ └── get_modal.tpl ├── snippets │ └── frontend │ │ └── swag_browser_language │ │ └── main.ini ├── services.xml └── config.xml ├── .php_cs.dist ├── LICENSE ├── Subscriber ├── Javascript.php └── Frontend.php ├── SwagBrowserLanguage.php ├── README.md ├── plugin.xml ├── Components ├── Translator.php └── ShopFinder.php └── Controllers └── Widgets └── SwagBrowserLanguage.php /.eslintignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shopware5/SwagBrowserLanguage/HEAD/image1.png -------------------------------------------------------------------------------- /plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shopware5/SwagBrowserLanguage/HEAD/plugin.png -------------------------------------------------------------------------------- /.githooks/install_hooks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 11 | 12 | {/block} 13 | {/block} -------------------------------------------------------------------------------- /Resources/snippets/frontend/swag_browser_language/main.ini: -------------------------------------------------------------------------------- 1 | [en_GB] 2 | modal/main_title = "Automatic forwarding available" 3 | modal/text = "We may automatically redirect you to the shop in your language.
If you don't want this, you can simply close this window." 4 | modal/go = "Go to the recommended shop" 5 | modal/close = "Close window" 6 | modal/choose = "Or choose a shop from below:" 7 | modal/recommendation = "Recommended shop:" 8 | 9 | [de_DE] 10 | modal/main_title = "Automatische Weiterleitung verfügbar" 11 | modal/text = "Wir können Sie automatisch auf den Shop Ihrer Sprache weiterleiten.
Ist das nicht gewünscht, können sie einfach dieses Fenster schließen" 12 | modal/go = "Zum empfohlenden Shop wechseln" 13 | modal/close = "Fenster schließen" 14 | modal/choose = "Oder wählen Sie hier einen Shop aus:" 15 | modal/recommendation = "Empfohlender shop:" -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | in(__DIR__) 5 | ; 6 | 7 | $header = << 9 | 10 | For the full copyright and license information, please view the LICENSE 11 | file that was distributed with this source code. 12 | EOF; 13 | 14 | return PhpCsFixer\Config::create() 15 | ->setUsingCache(false) 16 | ->setRules([ 17 | '@PSR2' => true, 18 | '@Symfony' => true, 19 | 'header_comment' => ['header' => $header, 'separate' => 'bottom', 'commentType' => 'PHPDoc'], 20 | 'no_useless_else' => true, 21 | 'no_useless_return' => true, 22 | 'ordered_class_elements' => true, 23 | 'ordered_imports' => true, 24 | 'phpdoc_order' => true, 25 | 'phpdoc_summary' => false, 26 | 'blank_line_after_opening_tag' => false, 27 | 'concat_space' => ['spacing' => 'one'], 28 | 'array_syntax' => ['syntax' => 'short'] 29 | ]) 30 | ->setFinder($finder) 31 | ; 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 ShopwareLabs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Subscriber/Javascript.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace SwagBrowserLanguage\Subscriber; 10 | 11 | use Doctrine\Common\Collections\ArrayCollection; 12 | use Enlight\Event\SubscriberInterface; 13 | 14 | class Javascript implements SubscriberInterface 15 | { 16 | /** 17 | * @var string 18 | */ 19 | private $viewDir; 20 | 21 | /** 22 | * @param string $viewDir 23 | */ 24 | public function __construct($viewDir) 25 | { 26 | $this->viewDir = $viewDir; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public static function getSubscribedEvents() 33 | { 34 | return [ 35 | 'Theme_Compiler_Collect_Plugin_Javascript' => 'addJsFiles', 36 | ]; 37 | } 38 | 39 | /** 40 | * Provide the needed javascript files 41 | * 42 | * @return ArrayCollection 43 | */ 44 | public function addJsFiles() 45 | { 46 | $jsPath = [ 47 | $this->viewDir . '/frontend/_public/src/js/jquery.redirect.js', 48 | ]; 49 | 50 | return new ArrayCollection($jsPath); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /SwagBrowserLanguage.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace SwagBrowserLanguage; 10 | 11 | use Shopware\Components\Plugin; 12 | use Shopware\Components\Plugin\Context\ActivateContext; 13 | use Shopware\Components\Plugin\Context\DeactivateContext; 14 | use Shopware\Components\Plugin\Context\UninstallContext; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | 17 | class SwagBrowserLanguage extends Plugin 18 | { 19 | /** 20 | * @param ContainerBuilder $container 21 | */ 22 | public function build(ContainerBuilder $container) 23 | { 24 | $container->setParameter('swag_browser_language.plugin_dir', $this->getPath()); 25 | parent::build($container); 26 | } 27 | 28 | /** 29 | * @param ActivateContext $context 30 | */ 31 | public function activate(ActivateContext $context) 32 | { 33 | $context->scheduleClearCache(ActivateContext::CACHE_LIST_FRONTEND); 34 | } 35 | 36 | /** 37 | * @param DeactivateContext $context 38 | */ 39 | public function deactivate(DeactivateContext $context) 40 | { 41 | $context->scheduleClearCache(DeactivateContext::CACHE_LIST_FRONTEND); 42 | } 43 | 44 | /** 45 | * @param UninstallContext $context 46 | */ 47 | public function uninstall(UninstallContext $context) 48 | { 49 | $context->scheduleClearCache(UninstallContext::CACHE_LIST_FRONTEND); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwagBrowserLanguage 2 | > Working with Shopware version 5.2.0 to 5.2.27. 3 | > Higher versions may work either but were not tested. 4 | > Use the version 1.1.1 for older Shopware versions 5 | 6 | ## Description 7 | This plugin automatically detects the language settings of your customer’s browsers and forwards them to the appropriate language or subshop. The browser languages are checked according to their priority. 8 | 9 | The following situations can occur: 10 | 11 | * The browser language corresponds to the language of your main shop (no forwarding) 12 | * The browser language matches one of the languages offered in your store (forwarded to the appropriate shop) 13 | * The browser language does not match any of the languages offered in your store (forwarded to the main shop, unless otherwise specified) 14 | * The browser language matches one of the subshops but the subshop has not been linked to the main shop (forwarded to the default shop) 15 | * In addition the user will be informed about the redirection before he's being redirected so he can cancel the redirection process manually at any time to stay at the current shop. Beside the proposal it is possible for the user to decide another shop than the suggested one. 16 | 17 | Your are missing a language or you found a missing or wrong translation? Help us to improve the available translations of shopware via [crowdin](../../../shopware.phpn.com/project/shopware) 18 | 19 | ## Images 20 | Backend 21 | 22 | ## License 23 | 24 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 25 | -------------------------------------------------------------------------------- /Resources/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | %swag_browser_language.plugin_dir%/Resources/views 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | %swag_browser_language.plugin_dir% 23 | 24 | 25 | 26 | %swag_browser_language.view_dir% 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Resources/views/widgets/swag_browser_language/get_modal.tpl: -------------------------------------------------------------------------------- 1 | {block name="frontend_index_browser_language_modal"} 2 | 38 | {/block} 39 | -------------------------------------------------------------------------------- /Subscriber/Frontend.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace SwagBrowserLanguage\Subscriber; 10 | 11 | use Enlight\Event\SubscriberInterface; 12 | use Enlight_Event_EventArgs; 13 | use Enlight_View_Default; 14 | use Shopware_Controllers_Frontend_Index; 15 | 16 | class Frontend implements SubscriberInterface 17 | { 18 | /** 19 | * @var string 20 | */ 21 | private $pluginDir; 22 | 23 | /** 24 | * @var array 25 | */ 26 | private $controllerWhiteList = ['detail', 'index', 'listing']; 27 | 28 | /** 29 | * @param string $pluginDir 30 | */ 31 | public function __construct($pluginDir) 32 | { 33 | $this->pluginDir = $pluginDir; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public static function getSubscribedEvents() 40 | { 41 | return [ 42 | 'Enlight_Controller_Dispatcher_ControllerPath_Widgets_SwagBrowserLanguage' => 'onGetFrontendController', 43 | 'Enlight_Controller_Action_PostDispatchSecure_Frontend' => 'onPostDispatchFrontend', 44 | ]; 45 | } 46 | 47 | /** 48 | * Returns the path to the frontend controller. 49 | * 50 | * @return string 51 | */ 52 | public function onGetFrontendController() 53 | { 54 | return $this->pluginDir . '/Controllers/Widgets/SwagBrowserLanguage.php'; 55 | } 56 | 57 | /** 58 | * Event listener function of the Enlight_Controller_Action_PostDispatch event. 59 | * 60 | * @param Enlight_Event_EventArgs $arguments 61 | */ 62 | public function onPostDispatchFrontend(Enlight_Event_EventArgs $arguments) 63 | { 64 | /** @var $controller Shopware_Controllers_Frontend_Index */ 65 | $controller = $arguments->get('subject'); 66 | 67 | /** @var \Enlight_Controller_Request_RequestHttp $request */ 68 | $request = $controller->Request(); 69 | 70 | if (!in_array($request->getControllerName(), $this->controllerWhiteList)) { 71 | return; 72 | } 73 | 74 | /** @var $view Enlight_View_Default */ 75 | $view = $controller->View(); 76 | 77 | $view->addTemplateDir($this->pluginDir . '/Resources/views'); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Resources/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | default 7 | 8 | 9 | Forward to this shop if not found any shop languages matching browser language 10 | Auf diesen Shop wird weitergeleitet, wenn kein zu den Browsersprachen passender Shop existiert. 11 | base.ShopLanguage 12 | 13 | 14 | assignedShops 15 | 16 | 17 | Forwards to these shops, if the browser language equals the shop language. 18 | Auf diese Shops wird weitergeleitet, wenn die Browsersprache der Shopsprache entspricht. 19 | base.ShopLanguage 20 | 21 | true 22 | 23 | 24 | 25 | fallbackLanguage 26 | 27 | 28 | en_GB 29 | This is the locale for the translation that will be used by default, if no matching translation was found for the user to be displayed in the infobox in the frontend. 30 | Dies ist die locale für die Übersetzung, auf die, wenn keine passende Übersetzung für die vom Benutzer gewählte Sprache existiert, zurückgegriffen wird, um die Infobox im Frontend zu übersetzen. 31 | 32 | 33 | forceBrowserMainLocale 34 | 35 | 36 | 0 37 | Additional locales on browser language will be ignored 38 | Zusätzliche Lokalisierungen werden in der Browsersprache ignoriert 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 2.0.1 7 | Shopware AG 8 | MIT 9 | 10 | Dieses Plugin entscheidet anhand der im Browser des Kunden eingestellten Sprachen,
auf welchen Sprachshop der Kunde weitergeleitet wird.
Die Browsersprachen werden dabei Ihrer Priorität nach geprüft.

12 |
13 |

Daraus ergeben sich folgende Fälle:

14 |
15 |
    16 |
  • Die gegenwärtig geprüfte Browsersprache entspricht dem Hauptshop
    -> keine Weiterleitung
  • 17 |
  • Die gegenwärtig geprüfte Browsersprache entspricht einem Sprachshop
    -> Weiterleitung auf den entsprechenden Sprachshop
  • 18 |
  • Keine der Browsersprachen entspricht einem Sprachshop
    -> Weiterleitung auf den in der Pluginkonfiguration eingestellten Fallback-Sprachshop, sofern dieser nicht dem Hauptshop entspricht, sonst keine Weiterleitung
  • 19 |
20 |
21 |

Sobald zu einer Sprache ein passender Shop gefunden wurde, wird die weitere Suche beendet.

22 |
23 |

Weiterhin besteht in der Pluginkonfiguraion die Möglichkeit eine Infobox zu aktivieren. Diese zeigt dem Benutzer im Falle einer durch das Plugin verursachten Weiterleitung einen kurzen Hinweis an und bietet die Möglichkeit zurück auf den Hauptshop zu gelangen.

24 | ]]> 25 |
26 | 27 | This plugin automatically detects the language settings of your customer’s browsers and forwards them to the appropriate language or subshop. The browser languages are checked according to their priority.

29 |
30 |

The following situations can occur:

31 |
32 |
    33 |
  • The browser language corresponds to the language of your main shop (no forwarding)
  • 34 |
  • The browser language matches one of the languages offered in your store (forwarded to the appropriate shop)
  • 35 |
  • The browser language does not match any of the languages offered in your store (forwarded to the main shop, unless otherwise specified)
  • 36 |
  • The browser language matches one of the subshops but the subshop has not been linked to the main shop (forwarded to the default shop)
  • 37 |
  • In addition the user will be informed about the redirection before he's being redirected so he can cancel the redirection process manually at any time to stay at the current shop. Beside the proposal it is possible for the user to decide another shop than the suggested one.
  • 38 |
      39 | ]]> 40 | 41 | http://store.shopware.com 42 | Shopware AG 43 | 44 | -------------------------------------------------------------------------------- /Resources/views/frontend/_public/src/js/jquery.redirect.js: -------------------------------------------------------------------------------- 1 | ;(function ($) { 2 | var sessionStorage = StorageManager.getSessionStorage(); 3 | 4 | $.plugin('swRedirectLang', { 5 | 6 | /** 7 | * The default options. 8 | */ 9 | defaults: { 10 | controllerName: 'index', 11 | 12 | moduleName: 'frontend', 13 | 14 | modalTitle: 'You can be redirected', 15 | 16 | modalURL: '' 17 | }, 18 | 19 | /** Plugin constructor */ 20 | init: function () { 21 | var me = this; 22 | 23 | me.applyDataAttributes(false); 24 | 25 | $.subscribe('plugin/swModal/onOpenAjax', $.proxy(me.onOpenModal, me)); 26 | 27 | if (!sessionStorage.getItem('swBrowserLanguage_redirected')) { 28 | me.handleRedirect(); 29 | } 30 | }, 31 | 32 | onOpenModal: function() { 33 | $('.modal--language-select').swSelectboxReplacement(); 34 | }, 35 | 36 | showModal: function() { 37 | var me = this, 38 | url = me.opts.modalURL; 39 | 40 | if (!url) { 41 | return; 42 | } 43 | 44 | $.get(url, function (response) { 45 | $.modal.open(response.content, { 46 | title: response.title, 47 | sizing: 'content' 48 | }); 49 | 50 | $($.modal._$modalBox).one('click.swag_browser_language', '.modal--close-button', function () { 51 | $.modal.close(); 52 | }); 53 | 54 | $($.modal._$modalBox).one('click.swag_browser_language', '.modal--go-button', function () { 55 | me.redirect(sessionStorage.getItem('swBrowserLanguage_destinationId')); 56 | }); 57 | 58 | $($.modal._$modalBox).on('change.swag_browser_language', '*[name="modal--combo-shops"]', function (event) { 59 | var $this = $(event.target), 60 | val = $this.val(); 61 | 62 | sessionStorage.setItem('swBrowserLanguage_destinationId', val); 63 | me.redirect(val); 64 | }); 65 | }, 'json'); 66 | }, 67 | 68 | handleRedirect: function() { 69 | var me = this; 70 | 71 | sessionStorage.setItem('swBrowserLanguage_redirected', true); 72 | 73 | $.ajax({ 74 | method: 'post', 75 | data: me.opts, 76 | url: me.$el.attr('data-redirectUrl'), 77 | success: function (response) { 78 | var data = JSON.parse(response); 79 | if (data.success === true) { 80 | if (data.destinationId) { 81 | sessionStorage.setItem('swBrowserLanguage_destinationId', data.destinationId); 82 | } 83 | 84 | me.showModal(); 85 | } 86 | } 87 | }); 88 | 89 | $.publish('swRedirect'); 90 | }, 91 | 92 | redirect: function(shopId) { 93 | $('
      ', { 94 | 'action': '', 95 | 'method': 'post', 96 | 'html': [ 97 | $('', { 98 | 'type': 'hidden', 99 | 'value': shopId, 100 | 'name': '__shop' 101 | }), 102 | $('', { 103 | 'type': 'hidden', 104 | 'value': 1, 105 | 'name': '__redirect' 106 | }), 107 | $('', { 108 | 'type': 'hidden', 109 | 'value': CSRF.getToken(), 110 | 'name': '__csrf_token' 111 | }) 112 | ] 113 | }).appendTo('body').submit(); 114 | }, 115 | 116 | /** Destroys the plugin */ 117 | destroy: function() { 118 | this._destroy(); 119 | } 120 | 121 | }); 122 | 123 | $(document).ready(function() { 124 | $('.language--redirect-container').swRedirectLang(); 125 | }); 126 | })(jQuery); 127 | -------------------------------------------------------------------------------- /Components/Translator.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace SwagBrowserLanguage\Components; 10 | 11 | use Enlight_Components_Db_Adapter_Pdo_Mysql; 12 | use Enlight_Components_Snippet_Namespace; 13 | use Shopware\Components\Model\ModelManager; 14 | use Shopware\Components\Plugin\CachedConfigReader; 15 | use Shopware\Models\Shop\Locale; 16 | use Shopware_Components_Snippet_Manager; 17 | 18 | class Translator 19 | { 20 | /** 21 | * Returns doctrine instance 22 | * 23 | * @var ModelManager 24 | */ 25 | private $models; 26 | 27 | /** 28 | * Returns the instance of the snippet manager 29 | * 30 | * @var Shopware_Components_Snippet_Manager 31 | */ 32 | private $snippets; 33 | 34 | /** 35 | * @var Enlight_Components_Db_Adapter_Pdo_Mysql 36 | */ 37 | private $db; 38 | 39 | /** 40 | * @var CachedConfigReader 41 | */ 42 | private $configReader; 43 | 44 | /** 45 | * @param ModelManager $models 46 | * @param Shopware_Components_Snippet_Manager $snippets 47 | * @param Enlight_Components_Db_Adapter_Pdo_Mysql $db 48 | * @param CachedConfigReader $configReader 49 | */ 50 | public function __construct( 51 | ModelManager $models, 52 | Shopware_Components_Snippet_Manager $snippets, 53 | Enlight_Components_Db_Adapter_Pdo_Mysql $db, 54 | CachedConfigReader $configReader 55 | ) { 56 | $this->models = $models; 57 | $this->snippets = $snippets; 58 | $this->db = $db; 59 | $this->configReader = $configReader; 60 | } 61 | 62 | /** 63 | * This function returns the snippets for a specific locale. 64 | * For example for the locale "en_GB" or "en" 65 | * 66 | * @param string $locale 67 | * 68 | * @return array 69 | */ 70 | public function getSnippets($locale) 71 | { 72 | $result = []; 73 | 74 | $localeId = $this->getLocaleId($locale[0]); 75 | 76 | $snippetNamespace = $this->getSnippetNamespace($localeId); 77 | 78 | if ((int) $snippetNamespace->count() === 0) { 79 | $snippetNamespace = $this->getSnippetNamespace($this->getLocaleId('en_GB')); 80 | } 81 | 82 | $result['choose'] = $snippetNamespace->get('modal/choose'); 83 | $result['close'] = $snippetNamespace->get('modal/close'); 84 | $result['go'] = $snippetNamespace->get('modal/go'); 85 | $result['title'] = $snippetNamespace->get('modal/main_title'); 86 | $result['recommendation'] = $snippetNamespace->get('modal/recommendation'); 87 | $result['text'] = $snippetNamespace->get('modal/text'); 88 | 89 | return $result; 90 | } 91 | 92 | /** 93 | * A helper function that returns a snippet namespace by a specific locale id 94 | * 95 | * @param $localeId 96 | * 97 | * @return Enlight_Components_Snippet_Namespace 98 | */ 99 | private function getSnippetNamespace($localeId) 100 | { 101 | /** @var Locale $locale */ 102 | $locale = $this->models->getRepository(Locale::class)->find($localeId); 103 | 104 | return $this->snippets->setLocale($locale)->getNamespace('frontend/swag_browser_language/main'); 105 | } 106 | 107 | /** 108 | * A helper function that returns the localeId of a locale string 109 | * 110 | * @param $locale 111 | * 112 | * @return int|null|string 113 | */ 114 | private function getLocaleId($locale) 115 | { 116 | $localeId = null; 117 | 118 | switch (strlen($locale)) { 119 | case 2: //sometimes only en, de, or es will be transmitted 120 | //Select the first matching language from the database 121 | $localeId = $this->db->fetchOne( 122 | 'SELECT `id` FROM `s_core_locales` WHERE `locale` LIKE :locale ORDER BY `id` ASC', 123 | ['locale' => $locale . '%'] 124 | ); 125 | break; 126 | case 5: // the standard format e.g en_GB 127 | $localeId = $this->db->fetchOne( 128 | 'SELECT `id` FROM `s_core_locales` WHERE `locale`=:locale', 129 | ['locale' => $locale] 130 | ); 131 | break; 132 | } 133 | 134 | if (null === $localeId) { 135 | $config = $this->configReader->getByPluginName('SwagBrowserLanguage'); 136 | $fallbackLocaleCode = $config['fallbackLanguage']; 137 | $localeId = $this->getLocaleId($fallbackLocaleCode); 138 | } 139 | 140 | //returns 2 (en_GB) if 1.) the browser language was not found and 2.) the fallback language was not found 141 | return null === $localeId ? 2 : $localeId; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | writeln(); 23 | $this->writeln('Checking commit requirements', 0); 24 | $this->writeln(); 25 | 26 | if ($this->isRebase()) { 27 | echo 'Not on branch' . PHP_EOL; 28 | 29 | return 0; 30 | } 31 | 32 | $this->runPhpLint($this->getCommittedFileList()); 33 | $this->runPhpCsFixer($this->getCommittedFileList()); 34 | $this->runEsLint($this->getCommittedFileList('js')); 35 | 36 | if ($this->error) { 37 | $this->writeln("If you are ABSOLUTELY sure your code is correct, you can use 'git commit --no-verify' to bypass this validation", 0); 38 | } 39 | 40 | exit((int) $this->error); 41 | } 42 | 43 | /** 44 | * @param string $output 45 | * @param int $level 46 | */ 47 | private function writeln($output = '', $level = 1) 48 | { 49 | $this->write($output, $level); 50 | echo PHP_EOL; 51 | } 52 | 53 | /** 54 | * @param string $output 55 | * @param int $level 56 | */ 57 | private function write($output = '', $level = 1) 58 | { 59 | $spaces = $level * 3; 60 | 61 | echo str_pad($output, strlen($output) + $spaces, ' ', STR_PAD_LEFT); 62 | } 63 | 64 | /** 65 | * @return bool 66 | */ 67 | private function isRebase() 68 | { 69 | $output = []; 70 | exec('git symbolic-ref --short -q HEAD', $output); 71 | 72 | return empty($output); 73 | } 74 | 75 | /** 76 | * @param string $extension 77 | * @return string[] 78 | */ 79 | private function getCommittedFileList($extension = 'php') 80 | { 81 | exec("git diff --name-only --diff-filter=ACMRTUXB \"HEAD\" | grep -e '\." . $extension . "$'", $fileList); 82 | 83 | return $fileList; 84 | } 85 | 86 | /** 87 | * @param array $fileList 88 | */ 89 | private function runPhpLint(array $fileList) 90 | { 91 | $this->writeln('# Checking php syntax'); 92 | $this->writeln('> php -l'); 93 | 94 | foreach ($fileList as $file) { 95 | exec('php -l ' . escapeshellarg($file) . ' 2> /dev/null', $output, $return); 96 | if ($return !== 0) { 97 | $this->writeln('- ' . $output[1], 2); 98 | $this->error = true; 99 | } 100 | } 101 | 102 | $this->writeln(); 103 | } 104 | 105 | /** 106 | * @param array $fileList 107 | */ 108 | private function runPhpCsFixer(array $fileList) 109 | { 110 | $this->writeln('# Checking php code style'); 111 | $this->writeln('> php-cs-fixer fix -v --no-ansi --dry-run'); 112 | 113 | if (!$this->isPHPCSFixerAvailable()) { 114 | $this->error = true; 115 | $this->writeln('- php-cs-fixer is NOT installed. Please install composer with dev dependencies.', 2); 116 | $this->writeln(); 117 | 118 | return; 119 | } 120 | 121 | foreach ($fileList as $file) { 122 | exec('./../../../vendor/bin/php-cs-fixer fix -v --no-ansi --dry-run ' . escapeshellarg($file) . ' 2>&1', $output, $return); 123 | 124 | if ($return !== 0) { 125 | $this->writeln('- ' . preg_replace('#^(\s+)?\d\)\s#', '', $output[3]), 2); 126 | $fixes[] = './../../../vendor/bin/php-cs-fixer fix -v ' . escapeshellarg($file); 127 | $this->error = true; 128 | } 129 | } 130 | 131 | if (!empty($fixes)) { 132 | $this->writeln(); 133 | $this->writeln('Help:', 2); 134 | foreach ($fixes as $fix) { 135 | $this->writeln($fix, 3); 136 | } 137 | } 138 | 139 | $this->writeln(); 140 | } 141 | 142 | /** 143 | * @param array $fileList 144 | */ 145 | private function runEsLint(array $fileList) 146 | { 147 | $this->writeln('# Checking javascript code style'); 148 | $this->writeln('> eslint.js --ignore-path .eslintignore'); 149 | 150 | if (!$this->isESLintAvailable()) { 151 | $this->writeln('- eslint.js not found. Skipping javascript code style check.', 2); 152 | $this->writeln(); 153 | 154 | return; 155 | } 156 | 157 | $this->checkESLint($fileList); 158 | 159 | $this->writeln(); 160 | } 161 | 162 | /** 163 | * @return bool 164 | */ 165 | private function isPHPCSFixerAvailable() 166 | { 167 | $output = []; 168 | $return = 0; 169 | exec('command -v ./../../../vendor/bin/php-cs-fixer >/dev/null 2>&1', $output, $return); 170 | 171 | return !(bool) $return; 172 | } 173 | 174 | /** 175 | * @return bool 176 | */ 177 | private function isESLintAvailable() 178 | { 179 | $output = []; 180 | $return = 0; 181 | exec('command -v ./../../../themes/node_modules/eslint/bin/eslint.js >/dev/null 2>&1', $output, $return); 182 | 183 | return !(bool) $return; 184 | } 185 | 186 | /** 187 | * @param array $fileList 188 | */ 189 | private function checkESLint(array $fileList = []) 190 | { 191 | $output = []; 192 | $return = 0; 193 | exec( 194 | './../../../themes/node_modules/eslint/bin/eslint.js ' . 195 | '--ignore-path .eslintignore ' . 196 | '-c ./../../../themes/.eslintrc.js ' . 197 | '--global "CSRF" ' . 198 | implode(' ', $fileList), 199 | $output, 200 | $return 201 | ); 202 | $return = !(bool) $return; 203 | 204 | if (!$return) { 205 | $this->error = true; 206 | 207 | foreach ($output as $line) { 208 | $this->writeln($line, 2); 209 | } 210 | 211 | $this->writeln('Help:', 2); 212 | $this->writeln( 213 | './../../../themes/node_modules/eslint/bin/eslint.js ' . 214 | '--fix --ignore-path .eslintignore ' . 215 | '-c ./../../../themes/.eslintrc.js ' . 216 | '--global "CSRF" ' . 217 | implode(' ', $fileList), 218 | 3 219 | ); 220 | } 221 | } 222 | } 223 | 224 | $checks = new PreCommitChecks(); 225 | $checks->run(); 226 | -------------------------------------------------------------------------------- /Components/ShopFinder.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace SwagBrowserLanguage\Components; 10 | 11 | use Shopware\Components\Model\ModelManager; 12 | use Shopware\Components\Plugin\CachedConfigReader; 13 | use Shopware\Models\Shop\Repository; 14 | use Shopware\Models\Shop\Shop; 15 | 16 | class ShopFinder 17 | { 18 | /** 19 | * @var array 20 | */ 21 | private $subShops; 22 | 23 | /** 24 | * @var ModelManager 25 | */ 26 | private $models; 27 | 28 | /** 29 | * @var array 30 | */ 31 | private $pluginConfig; 32 | 33 | /** 34 | * @param ModelManager $models 35 | * @param CachedConfigReader $configReader 36 | */ 37 | public function __construct(ModelManager $models, CachedConfigReader $configReader) 38 | { 39 | $this->models = $models; 40 | $this->subShops = $this->getLanguageShops(); 41 | $this->pluginConfig = $configReader->getByPluginName('SwagBrowserLanguage'); 42 | } 43 | 44 | /** 45 | * Helper function to get the SubshopId of the Shop in the preferred language 46 | * 47 | * @param $languages 48 | * 49 | * @return mixed 50 | */ 51 | public function getSubshopId($languages) 52 | { 53 | $assignedShops = array_values($this->pluginConfig['assignedShops']); 54 | 55 | if (!$assignedShops) { 56 | return $this->getDefaultShopId(); 57 | } 58 | 59 | $subShopId = $this->getSubShopIdByFullBrowserLanguage($languages, $assignedShops); 60 | 61 | if (!$subShopId) { 62 | $subShopId = $this->getSubShopIdByBrowserLanguagePrefix($languages, $assignedShops); 63 | } 64 | if (!$subShopId) { 65 | $subShopId = $this->getDefaultShopId(); 66 | } 67 | 68 | if (!in_array($subShopId, $assignedShops)) { 69 | return $this->getDefaultShopId(); 70 | } 71 | 72 | return $subShopId; 73 | } 74 | 75 | /** 76 | * @return array 77 | */ 78 | public function getSubShops() 79 | { 80 | return $this->subShops; 81 | } 82 | 83 | /** 84 | * @return mixed 85 | */ 86 | public function getFirstSubshopId() 87 | { 88 | return $this->subShops[0]['id']; 89 | } 90 | 91 | /** 92 | * @param $subshopId 93 | * 94 | * @return Shop 95 | */ 96 | public function getShopRepository($subshopId) 97 | { 98 | /** @var Repository $repository */ 99 | $repository = $this->models->getRepository(Shop::class); 100 | 101 | return $repository->getActiveById($subshopId); 102 | } 103 | 104 | /** 105 | * Helper method that creates an array of shop information that may be used in the modal box. 106 | * 107 | * @param $subShopIds 108 | * 109 | * @return array 110 | */ 111 | public function getShopsForModal($subShopIds) 112 | { 113 | $resultArray = []; 114 | foreach ($subShopIds as $subShopId) { 115 | $model = $this->getShopRepository($subShopId); 116 | $resultArray[$subShopId] = $model->getName(); 117 | } 118 | 119 | return $resultArray; 120 | } 121 | 122 | /** 123 | * HelperMethod for getSubShopId... get the default ShopId 124 | * 125 | * @return int 126 | */ 127 | private function getDefaultShopId() 128 | { 129 | $default = $this->pluginConfig['default']; 130 | if (!is_int($default)) { 131 | $default = $this->getFirstSubshopId(); 132 | } 133 | 134 | return $default; 135 | } 136 | 137 | /** 138 | * HelperMethod for getSubShopId... try to get the LanguageShop by the full BrowserLanguage like [de-DE] or [de-CH] 139 | * 140 | * @param $languages 141 | * @param $assignedShops 142 | * 143 | * @return bool 144 | */ 145 | private function getSubShopIdByFullBrowserLanguage($languages, $assignedShops) 146 | { 147 | foreach ($languages as $language) { 148 | $browserLanguage = strtolower($language); 149 | 150 | foreach ($this->subShops as $subshop) { 151 | $shopLocale = strtolower($subshop['locale']); 152 | 153 | if ($browserLanguage === $shopLocale && in_array($subshop['id'], $assignedShops)) { 154 | return $subshop['id']; 155 | } 156 | } 157 | } 158 | 159 | return false; 160 | } 161 | 162 | /** 163 | * HelperMethod for getSubShopId... try to get the LanguageShop by the BrowserLanguage "prefix" like [de] or [en] 164 | * 165 | * @param $languages 166 | * @param $assignedShops 167 | * 168 | * @return bool 169 | */ 170 | private function getSubShopIdByBrowserLanguagePrefix($languages, $assignedShops) 171 | { 172 | foreach ($languages as $language) { 173 | $browserLanguage = strtolower($language); 174 | $currentLanguageArray = explode('-', $browserLanguage); 175 | $browserLanguagePrefix = $currentLanguageArray[0]; 176 | 177 | foreach ($this->subShops as $subshop) { 178 | $subshopLanguage = $subshop['language']; 179 | 180 | if ($browserLanguagePrefix === $subshopLanguage && in_array($subshop['id'], $assignedShops)) { 181 | return $subshop['id']; 182 | } 183 | } 184 | } 185 | 186 | return false; 187 | } 188 | 189 | /** 190 | * Helper function to get the needed data of all active language shops (optional: of a main shop) 191 | * 192 | * @return array 193 | */ 194 | private function getLanguageShops() 195 | { 196 | $data = $this->getData(); 197 | $subshops = []; 198 | 199 | foreach ($data as $subshop) { 200 | $subshop['locale'] = strtolower($subshop['locale']['locale']); 201 | $subshop['locale'] = str_replace('_', '-', $subshop['locale']); 202 | 203 | $subshop['language'] = explode('-', $subshop['locale']); 204 | $subshop['language'] = $subshop['language'][0]; 205 | 206 | $subshops[] = [ 207 | 'id' => $subshop['id'], 208 | 'name' => $subshop['name'], 209 | 'locale' => $subshop['locale'], 210 | 'language' => $subshop['language'], 211 | ]; 212 | } 213 | 214 | return $subshops; 215 | } 216 | 217 | /** 218 | * Helper function that queries all sub shops (including language sub shops). 219 | * 220 | * @return array 221 | */ 222 | private function getData() 223 | { 224 | /** @var Repository $repository */ 225 | $repository = $this->models->getRepository(Shop::class); 226 | $builder = $repository->getActiveQueryBuilder(); 227 | $builder->orderBy('shop.id'); 228 | 229 | return $builder->getQuery()->getArrayResult(); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /Controllers/Widgets/SwagBrowserLanguage.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | use Shopware\Components\CSRFWhitelistAware; 10 | use SwagBrowserLanguage\Components\ShopFinder; 11 | use SwagBrowserLanguage\Components\Translator; 12 | 13 | class Shopware_Controllers_Widgets_SwagBrowserLanguage extends Enlight_Controller_Action implements CSRFWhitelistAware 14 | { 15 | /** 16 | * @var array 17 | */ 18 | private $controllerWhiteList = ['detail', 'index', 'listing']; 19 | 20 | /** 21 | * @var ShopFinder 22 | */ 23 | private $shopFinder; 24 | 25 | /** 26 | * @var Enlight_Components_Session_Namespace 27 | */ 28 | private $session; 29 | 30 | /** 31 | * @var Translator 32 | */ 33 | private $translator; 34 | 35 | /** 36 | * @var \Shopware\Models\Shop\Shop null 37 | */ 38 | private $shop; 39 | 40 | /** @var array */ 41 | private $config; 42 | 43 | /** 44 | * Returns a list with actions which should not be validated for CSRF protection 45 | * 46 | * @return string[] 47 | */ 48 | public function getWhitelistedCSRFActions() 49 | { 50 | return [ 51 | 'redirect', 52 | ]; 53 | } 54 | 55 | /** 56 | * This function will be called before the widget is being finalized 57 | */ 58 | public function preDispatch() 59 | { 60 | $this->shopFinder = $this->get('swag_browser_language.components.shop_finder'); 61 | $this->translator = $this->get('swag_browser_language.components.translator'); 62 | $this->session = $this->get('session'); 63 | $this->shop = $this->get('shop'); 64 | 65 | $this->View()->addTemplateDir($this->container->getParameter('swag_browser_language.view_dir')); 66 | 67 | parent::preDispatch(); 68 | } 69 | 70 | /** 71 | * Action to return the url for the redirection to javascript. 72 | * Won't return anything if there is no redirection needed. 73 | */ 74 | public function redirectAction() 75 | { 76 | $this->get('Front')->Plugins()->ViewRenderer()->setNoRender(); 77 | $request = $this->Request(); 78 | 79 | if ($this->session->get('Bot')) { 80 | echo json_encode([ 81 | 'success' => false, 82 | ]); 83 | 84 | return; 85 | } 86 | 87 | $languages = $this->getBrowserLanguages($request); 88 | 89 | $currentLocale = $this->shop->getLocale()->getLocale(); 90 | $currentLanguage = explode('_', $currentLocale); 91 | 92 | //Does this shop have the browser language already? 93 | if (in_array($currentLocale, $languages) || in_array($currentLanguage[0], $languages)) { 94 | echo json_encode([ 95 | 'success' => false, 96 | ]); 97 | 98 | return; 99 | } 100 | 101 | if (!$this->allowRedirect($this->Request()->getPost())) { 102 | echo json_encode([ 103 | 'success' => false, 104 | ]); 105 | 106 | return; 107 | } 108 | 109 | $subShopId = $this->shopFinder->getSubshopId($languages); 110 | 111 | //If the current shop is the destination shop do not redirect 112 | if ($this->shop->getId() == $subShopId) { 113 | echo json_encode([ 114 | 'success' => false, 115 | ]); 116 | 117 | return; 118 | } 119 | 120 | echo json_encode([ 121 | 'success' => true, 122 | 'destinationId' => $subShopId, 123 | ]); 124 | } 125 | 126 | /** 127 | * This action displays the content of the modal box 128 | */ 129 | public function getModalAction() 130 | { 131 | $this->get('Front')->Plugins()->ViewRenderer()->setNoRender(); 132 | $request = $this->Request(); 133 | $languages = $this->getBrowserLanguages($request); 134 | $subShopId = $this->shopFinder->getSubshopId($languages); 135 | 136 | $assignedShops = $this->getPluginConfig()['assignedShops']; 137 | $shopsToDisplay = $this->shopFinder->getShopsForModal($assignedShops); 138 | 139 | $snippets = $this->translator->getSnippets($languages); 140 | 141 | $this->View()->assign('snippets', $snippets); 142 | $this->View()->assign('shops', $shopsToDisplay); 143 | $this->View()->assign('destinationShop', $this->shopFinder->getShopRepository($subShopId)->getName()); 144 | $this->View()->assign('destinationId', $subShopId); 145 | 146 | echo json_encode([ 147 | 'title' => $snippets['title'], 148 | 'content' => $this->View()->fetch('widgets/swag_browser_language/get_modal.tpl'), 149 | ]); 150 | } 151 | 152 | /** 153 | * Helper function to get all preferred browser languages 154 | * 155 | * @param Enlight_Controller_Request_Request $request 156 | * 157 | * @return array|mixed 158 | */ 159 | private function getBrowserLanguages(Enlight_Controller_Request_Request $request) 160 | { 161 | $languages = $request->getServer('HTTP_ACCEPT_LANGUAGE'); 162 | $languages = str_replace('-', '_', $languages); 163 | 164 | if (strpos($languages, ',') == true) { 165 | $languages = explode(',', $languages); 166 | } else { 167 | $languages = (array) $languages; 168 | } 169 | 170 | foreach ($languages as $key => $language) { 171 | $language = explode(';', $language); 172 | $languages[$key] = $language[0]; 173 | } 174 | 175 | if ($this->getPluginConfig()['forceBrowserMainLocale'] && count($languages) > 2) { 176 | $languages = [ 177 | $languages[0], 178 | $languages[1], 179 | ]; 180 | } 181 | 182 | return (array) $languages; 183 | } 184 | 185 | /** 186 | * Make sure that only useful redirects are performed 187 | * 188 | * @param array $params 189 | * 190 | * @return bool 191 | */ 192 | private function allowRedirect($params) 193 | { 194 | $module = $params['moduleName'] ?: 'frontend'; 195 | $controllerName = $params['controllerName'] ?: 'index'; 196 | 197 | // Only process frontend requests 198 | if ($module !== 'frontend') { 199 | return false; 200 | } 201 | 202 | // check whitelist 203 | if (!in_array($controllerName, $this->controllerWhiteList)) { 204 | return false; 205 | } 206 | 207 | // don't redirect payment controllers 208 | if ($controllerName === 'payment') { 209 | return false; 210 | } 211 | 212 | return true; 213 | } 214 | 215 | /** 216 | * @return array|mixed 217 | */ 218 | private function getPluginConfig() 219 | { 220 | if ($this->config === null) { 221 | $this->config = $this->get('shopware.plugin.cached_config_reader')->getByPluginName('SwagBrowserLanguage'); 222 | } 223 | 224 | return $this->config; 225 | } 226 | } 227 | --------------------------------------------------------------------------------