1<?php
2/**
3 * Copyright since 2007 PrestaShop SA and Contributors
4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
5 *
6 * NOTICE OF LICENSE
7 *
8 * This source file is subject to the Open Software License (OSL 3.0)
9 * that is bundled with this package in the file LICENSE.md.
10 * It is also available through the world-wide-web at this URL:
11 * https://opensource.org/licenses/OSL-3.0
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@prestashop.com so we can send you a copy immediately.
15 *
16 * DISCLAIMER
17 *
18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
19 * versions in the future. If you wish to customize PrestaShop for your
20 * needs please refer to https://devdocs.prestashop.com/ for more information.
21 *
22 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
23 * @copyright Since 2007 PrestaShop SA and Contributors
24 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
25 */
26
27use PrestaShop\PrestaShop\Adapter\ContainerFinder;
28use PrestaShop\PrestaShop\Adapter\Module\Repository\ModuleRepository;
29use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
30use PrestaShop\PrestaShop\Core\Exception\ContainerNotFoundException;
31use PrestaShop\PrestaShop\Core\Localization\CLDR\ComputingPrecision;
32use PrestaShop\PrestaShop\Core\Localization\Locale;
33use PrestaShopBundle\Install\Language as InstallLanguage;
34use PrestaShopBundle\Translation\TranslatorComponent as Translator;
35use PrestaShopBundle\Translation\TranslatorLanguageLoader;
36use Symfony\Component\DependencyInjection\ContainerBuilder;
37use Symfony\Component\Filesystem\Filesystem;
38use Symfony\Component\Finder\Finder;
39use Symfony\Component\HttpFoundation\Session\SessionInterface;
40
41/**
42 * Class ContextCore.
43 *
44 * @since 1.5.0.1
45 */
46class ContextCore
47{
48    /** @var Context */
49    protected static $instance;
50
51    /** @var Cart */
52    public $cart;
53
54    /** @var Customer */
55    public $customer;
56
57    /** @var Cookie */
58    public $cookie;
59
60    /** @var SessionInterface|null */
61    public $session;
62
63    /** @var Link */
64    public $link;
65
66    /** @var Country */
67    public $country;
68
69    /** @var Employee|null */
70    public $employee;
71
72    /** @var AdminController|FrontController */
73    public $controller;
74
75    /** @var string */
76    public $override_controller_name_for_translations;
77
78    /** @var Language|InstallLanguage */
79    public $language;
80
81    /** @var Currency|null */
82    public $currency;
83
84    /**
85     * Current locale instance.
86     *
87     * @var Locale|null
88     */
89    public $currentLocale;
90
91    /** @var Tab */
92    public $tab;
93
94    /** @var Shop */
95    public $shop;
96
97    /** @var Smarty */
98    public $smarty;
99
100    /** @var \Mobile_Detect */
101    public $mobile_detect;
102
103    /** @var int */
104    public $mode;
105
106    /** @var ContainerBuilder */
107    public $container;
108
109    /** @var Translator */
110    protected $translator = null;
111
112    /** @var int */
113    protected $priceComputingPrecision = null;
114
115    /**
116     * Mobile device of the customer.
117     *
118     * @var bool|null
119     */
120    protected $mobile_device = null;
121
122    /** @var bool|null */
123    protected $is_mobile = null;
124
125    /** @var bool|null */
126    protected $is_tablet = null;
127
128    /** @var int */
129    const DEVICE_COMPUTER = 1;
130
131    /** @var int */
132    const DEVICE_TABLET = 2;
133
134    /** @var int */
135    const DEVICE_MOBILE = 4;
136
137    /** @var int */
138    const MODE_STD = 1;
139
140    /** @var int */
141    const MODE_STD_CONTRIB = 2;
142
143    /** @var int */
144    const MODE_HOST_CONTRIB = 4;
145
146    /** @var int */
147    const MODE_HOST = 8;
148
149    /**
150     * Sets Mobile_Detect tool object.
151     *
152     * @return Mobile_Detect
153     */
154    public function getMobileDetect()
155    {
156        if ($this->mobile_detect === null) {
157            $this->mobile_detect = new Mobile_Detect();
158        }
159
160        return $this->mobile_detect;
161    }
162
163    /**
164     * Checks if visitor's device is a mobile device.
165     *
166     * @return bool
167     */
168    public function isMobile()
169    {
170        if ($this->is_mobile === null) {
171            $mobileDetect = $this->getMobileDetect();
172            $this->is_mobile = $mobileDetect->isMobile();
173        }
174
175        return $this->is_mobile;
176    }
177
178    /**
179     * Checks if visitor's device is a tablet device.
180     *
181     * @return bool
182     */
183    public function isTablet()
184    {
185        if ($this->is_tablet === null) {
186            $mobileDetect = $this->getMobileDetect();
187            $this->is_tablet = $mobileDetect->isTablet();
188        }
189
190        return $this->is_tablet;
191    }
192
193    /**
194     * Sets mobile_device context variable.
195     *
196     * @return bool
197     */
198    public function getMobileDevice()
199    {
200        if ($this->mobile_device === null) {
201            $this->mobile_device = false;
202            if ($this->checkMobileContext()) {
203                if (isset(Context::getContext()->cookie->no_mobile) && Context::getContext()->cookie->no_mobile == false && (int) Configuration::get('PS_ALLOW_MOBILE_DEVICE') != 0) {
204                    $this->mobile_device = true;
205                } else {
206                    switch ((int) Configuration::get('PS_ALLOW_MOBILE_DEVICE')) {
207                        case 1: // Only for mobile device
208                            if ($this->isMobile() && !$this->isTablet()) {
209                                $this->mobile_device = true;
210                            }
211
212                            break;
213                        case 2: // Only for touchpads
214                            if ($this->isTablet() && !$this->isMobile()) {
215                                $this->mobile_device = true;
216                            }
217
218                            break;
219                        case 3: // For touchpad or mobile devices
220                            if ($this->isMobile() || $this->isTablet()) {
221                                $this->mobile_device = true;
222                            }
223
224                            break;
225                    }
226                }
227            }
228        }
229
230        return $this->mobile_device;
231    }
232
233    /**
234     * Returns mobile device type.
235     *
236     * @return int
237     */
238    public function getDevice()
239    {
240        static $device = null;
241
242        if ($device === null) {
243            if ($this->isTablet()) {
244                $device = Context::DEVICE_TABLET;
245            } elseif ($this->isMobile()) {
246                $device = Context::DEVICE_MOBILE;
247            } else {
248                $device = Context::DEVICE_COMPUTER;
249            }
250        }
251
252        return $device;
253    }
254
255    /**
256     * @return Locale|null
257     */
258    public function getCurrentLocale()
259    {
260        return $this->currentLocale;
261    }
262
263    /**
264     * Checks if mobile context is possible.
265     *
266     * @return bool
267     *
268     * @throws PrestaShopException
269     */
270    protected function checkMobileContext()
271    {
272        // Check mobile context
273        if (Tools::isSubmit('no_mobile_theme')) {
274            Context::getContext()->cookie->no_mobile = true;
275            if (Context::getContext()->cookie->id_guest) {
276                $guest = new Guest(Context::getContext()->cookie->id_guest);
277                $guest->mobile_theme = false;
278                $guest->update();
279            }
280        } elseif (Tools::isSubmit('mobile_theme_ok')) {
281            Context::getContext()->cookie->no_mobile = false;
282            if (Context::getContext()->cookie->id_guest) {
283                $guest = new Guest(Context::getContext()->cookie->id_guest);
284                $guest->mobile_theme = true;
285                $guest->update();
286            }
287        }
288
289        return isset($_SERVER['HTTP_USER_AGENT'], Context::getContext()->cookie)
290            && (bool) Configuration::get('PS_ALLOW_MOBILE_DEVICE')
291            && @filemtime(_PS_THEME_MOBILE_DIR_)
292            && !Context::getContext()->cookie->no_mobile;
293    }
294
295    /**
296     * Get a singleton instance of Context object.
297     *
298     * @return Context|null
299     */
300    public static function getContext()
301    {
302        if (!isset(self::$instance)) {
303            self::$instance = new Context();
304        }
305
306        return self::$instance;
307    }
308
309    /**
310     * @param $testInstance Context
311     * Unit testing purpose only
312     */
313    public static function setInstanceForTesting($testInstance)
314    {
315        self::$instance = $testInstance;
316    }
317
318    /**
319     * Unit testing purpose only.
320     */
321    public static function deleteTestingInstance()
322    {
323        self::$instance = null;
324    }
325
326    /**
327     * Clone current context object.
328     *
329     * @return Context
330     */
331    public function cloneContext()
332    {
333        return clone $this;
334    }
335
336    /**
337     * Update context after customer login.
338     *
339     * @param Customer $customer Created customer
340     */
341    public function updateCustomer(Customer $customer)
342    {
343        $this->customer = $customer;
344        $this->cookie->id_customer = (int) $customer->id;
345        $this->cookie->customer_lastname = $customer->lastname;
346        $this->cookie->customer_firstname = $customer->firstname;
347        $this->cookie->passwd = $customer->passwd;
348        $this->cookie->logged = 1;
349        $customer->logged = 1;
350        $this->cookie->email = $customer->email;
351        $this->cookie->is_guest = $customer->isGuest();
352
353        if (Configuration::get('PS_CART_FOLLOWING') && (empty($this->cookie->id_cart) || Cart::getNbProducts($this->cookie->id_cart) == 0) && $idCart = (int) Cart::lastNoneOrderedCart($this->customer->id)) {
354            $this->cart = new Cart($idCart);
355            $this->cart->secure_key = $customer->secure_key;
356        } else {
357            $idCarrier = (int) $this->cart->id_carrier;
358            $this->cart->secure_key = $customer->secure_key;
359            $this->cart->id_carrier = 0;
360            $this->cart->setDeliveryOption(null);
361            $this->cart->updateAddressId($this->cart->id_address_delivery, (int) Address::getFirstCustomerAddressId((int) ($customer->id)));
362            $this->cart->id_address_delivery = (int) Address::getFirstCustomerAddressId((int) ($customer->id));
363            $this->cart->id_address_invoice = (int) Address::getFirstCustomerAddressId((int) ($customer->id));
364        }
365        $this->cart->id_customer = (int) $customer->id;
366
367        if (isset($idCarrier) && $idCarrier) {
368            $deliveryOption = [$this->cart->id_address_delivery => $idCarrier . ','];
369            $this->cart->setDeliveryOption($deliveryOption);
370        }
371
372        $this->cart->save();
373        $this->cookie->id_cart = (int) $this->cart->id;
374        $this->cookie->write();
375        $this->cart->autosetProductAddress();
376
377        $this->cookie->registerSession(new CustomerSession());
378    }
379
380    /**
381     * Returns a translator depending on service container availability and if the method
382     * is called by the installer or not.
383     *
384     * @param bool $isInstaller Set to true if the method is called by the installer
385     *
386     * @return Translator
387     */
388    public function getTranslator($isInstaller = false)
389    {
390        if (null !== $this->translator && $this->language->locale === $this->translator->getLocale()) {
391            return $this->translator;
392        }
393
394        $sfContainer = SymfonyContainer::getInstance();
395
396        if ($isInstaller || null === $sfContainer) {
397            // symfony's container isn't available in front office, so we load and configure the translator component
398            $this->translator = $this->getTranslatorFromLocale($this->language->locale);
399        } else {
400            $this->translator = $sfContainer->get('translator');
401            // We need to set the locale here because in legacy BO pages, the translator is used
402            // before the TranslatorListener does its job of setting the locale according to the Request object
403            $this->translator->setLocale($this->language->locale);
404        }
405
406        return $this->translator;
407    }
408
409    /**
410     * Returns a new instance of Translator for the provided locale code.
411     *
412     * @param string $locale IETF language tag (eg. "en-US")
413     *
414     * @return Translator
415     */
416    public function getTranslatorFromLocale($locale)
417    {
418        $cacheDir = _PS_CACHE_DIR_ . 'translations';
419        $translator = new Translator($locale, null, $cacheDir, false);
420
421        // In case we have at least 1 translated message, we return the current translator.
422        // If some translations are missing, clear cache
423        if ($locale === '' || null === $locale || count($translator->getCatalogue($locale)->all())) {
424            return $translator;
425        }
426
427        // However, in some case, even empty catalog were stored in the cache and then used as-is.
428        // For this one, we drop the cache and try to regenerate it.
429        if (is_dir($cacheDir)) {
430            $cache_file = Finder::create()
431                ->files()
432                ->in($cacheDir)
433                ->depth('==0')
434                ->name('*.' . $locale . '.*');
435            (new Filesystem())->remove($cache_file);
436        }
437
438        $translator->clearLanguage($locale);
439
440        $adminContext = defined('_PS_ADMIN_DIR_');
441        // Do not load DB translations when $this->language is InstallLanguage
442        // because it means that we're looking for the installer translations, so we're not yet connected to the DB
443        $withDB = !$this->language instanceof InstallLanguage;
444        $theme = $this->shop !== null ? $this->shop->theme : null;
445
446        try {
447            $containerFinder = new ContainerFinder($this);
448            $containerFinder->getContainer()->get('prestashop.translation.translator_language_loader')
449                ->setIsAdminContext($adminContext)
450                ->loadLanguage($translator, $locale, $withDB, $theme);
451        } catch (ContainerNotFoundException $exception) {
452            // If a container is still not found, instantiate manually the translator loader
453            // This will happen in the Front as we have legacy controllers, the Sf container won't be available.
454            // 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
455            (new TranslatorLanguageLoader(new ModuleRepository()))
456                ->setIsAdminContext($adminContext)
457                ->loadLanguage($translator, $locale, $withDB, $theme);
458        }
459
460        return $translator;
461    }
462
463    /**
464     * @return array
465     */
466    protected function getTranslationResourcesDirectories()
467    {
468        $locations = [_PS_ROOT_DIR_ . '/app/Resources/translations'];
469
470        if (null !== $this->shop) {
471            $activeThemeLocation = _PS_ROOT_DIR_ . '/themes/' . $this->shop->theme_name . '/translations';
472            if (is_dir($activeThemeLocation)) {
473                $locations[] = $activeThemeLocation;
474            }
475        }
476
477        return $locations;
478    }
479
480    /**
481     * Returns the computing precision according to the current currency
482     *
483     * @return int
484     */
485    public function getComputingPrecision()
486    {
487        if ($this->priceComputingPrecision === null) {
488            $computingPrecision = new ComputingPrecision();
489            $this->priceComputingPrecision = $computingPrecision->getPrecision($this->currency->precision);
490        }
491
492        return $this->priceComputingPrecision;
493    }
494}
495