Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.66% covered (success)
92.66%
101 / 109
66.67% covered (warning)
66.67%
8 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ClientConfigStorage
92.66% covered (success)
92.66%
101 / 109
66.67% covered (warning)
66.67%
8 / 12
25.25
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 setConfigByPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getConfig
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
1 / 1
2
 getUserData
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 getClientTimeout
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isCrossOrigin
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getExtension
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
3.19
 getShownExtension
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getShownStructure
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 getLibConfigs
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getLang
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getEncodedValue
25.00% covered (danger)
25.00%
1 / 4
0.00% covered (danger)
0.00%
0 / 1
3.69
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2023-2024 (original work) Open Assessment Technologies SA.
19 *
20 * @author Andrei Shapiro <andrei.shapiro@taotesting.com>
21 */
22
23declare(strict_types=1);
24
25namespace oat\tao\model\clientConfig;
26
27use common_ext_Extension;
28use common_ext_ExtensionException;
29use common_ext_ExtensionsManager;
30use common_session_Session;
31use Exception;
32use oat\generis\model\user\UserRdf;
33use oat\oatbox\session\SessionService;
34use oat\oatbox\user\UserLanguageServiceInterface;
35use oat\tao\helpers\dateFormatter\DateFormatterFactory;
36use oat\tao\model\asset\AssetService;
37use oat\tao\model\ClientLibRegistry;
38use oat\tao\model\CookiePolicy\Service\CookiePolicyConfigurationRetriever;
39use oat\tao\model\featureFlag\FeatureFlagConfigSwitcher;
40use oat\tao\model\featureFlag\Repository\FeatureFlagRepositoryInterface;
41use oat\tao\model\menu\MenuService;
42use oat\tao\model\routing\ResolverFactory;
43use oat\tao\model\security\xsrf\TokenService;
44use oat\tao\model\session\Context\UserDataSessionContext;
45use Psr\Log\LoggerInterface;
46use tao_helpers_Date;
47use tao_helpers_Mode;
48use Throwable;
49
50class ClientConfigStorage
51{
52    private TokenService $tokenService;
53    private ClientLibRegistry $clientLibRegistry;
54    private FeatureFlagConfigSwitcher $featureFlagConfigSwitcher;
55    private AssetService $assetService;
56    private common_ext_ExtensionsManager $extensionsManager;
57    private ClientConfigService $clientConfigService;
58    private UserLanguageServiceInterface $userLanguageService;
59    private FeatureFlagRepositoryInterface $featureFlagRepository;
60    private ResolverFactory $resolverFactory;
61    private LoggerInterface $logger;
62    private SessionService $sessionService;
63    private tao_helpers_Mode $modeHelper;
64    private DateFormatterFactory $dateFormatterFactory;
65    private MenuService $menuService;
66
67    private array $config = [];
68    private CookiePolicyConfigurationRetriever $cookiePolicyConfigurationRetriever;
69
70    public function __construct(
71        TokenService $tokenService,
72        ClientLibRegistry $clientLibRegistry,
73        FeatureFlagConfigSwitcher $featureFlagConfigSwitcher,
74        AssetService $assetService,
75        common_ext_ExtensionsManager $extensionsManager,
76        ClientConfigService $clientConfigService,
77        UserLanguageServiceInterface $userLanguageService,
78        FeatureFlagRepositoryInterface $featureFlagRepository,
79        ResolverFactory $resolverFactory,
80        LoggerInterface $logger,
81        SessionService $sessionService,
82        tao_helpers_Mode $modeHelper,
83        DateFormatterFactory $dateFormatterFactory,
84        MenuService $menuService,
85        CookiePolicyConfigurationRetriever $cookiePolicyConfigurationRetriever
86    ) {
87        $this->tokenService = $tokenService;
88        $this->clientLibRegistry = $clientLibRegistry;
89        $this->featureFlagConfigSwitcher = $featureFlagConfigSwitcher;
90        $this->assetService = $assetService;
91        $this->extensionsManager = $extensionsManager;
92        $this->clientConfigService = $clientConfigService;
93        $this->userLanguageService = $userLanguageService;
94        $this->featureFlagRepository = $featureFlagRepository;
95        $this->resolverFactory = $resolverFactory;
96        $this->logger = $logger;
97        $this->sessionService = $sessionService;
98        $this->modeHelper = $modeHelper;
99        $this->dateFormatterFactory = $dateFormatterFactory;
100        $this->menuService = $menuService;
101        $this->cookiePolicyConfigurationRetriever = $cookiePolicyConfigurationRetriever;
102    }
103
104    /**
105     * Using the DI container you can set configs by providing config path and it's values using environment variables
106     * and ect.
107     *
108     * $services
109     *     ->get(ClientConfigStorage::class)
110     *     ->call(
111     *         'setConfigByPath',
112     *         [
113     *             [
114     *                 'libConfigs' => [
115     *                     'somePath' => [
116     *                         'someProp' => env('SOME_ENV_VARIABLE')->string(),
117     *                     ],
118     *                 ],
119     *             ],
120     *         ]
121     *     )
122     *     ->call(
123     *         'setConfigByPath',
124     *         [
125     *             [
126     *                 'context' => [
127     *                     'somePath' => [
128     *                         'someProp' => env('ANOTHER_ENV_VARIABLE')->int(),
129     *                     ],
130     *                 ],
131     *             ],
132     *         ]
133     *     );
134     */
135    public function setConfigByPath(array $path): void
136    {
137        $this->config = array_merge_recursive($this->config, $path);
138    }
139
140    public function getConfig(GetConfigQuery $query): array
141    {
142        $resolver = $this->resolverFactory->create([
143            'extension' => $query->getExtension(),
144            'action' => $query->getAction(),
145            'module' => $query->getModule(),
146        ]);
147
148        $currentSession = $this->sessionService->getCurrentSession();
149        $taoBaseWww = $this->assetService->getJsBaseWww('tao');
150        $langCode = $currentSession->getInterfaceLanguage();
151        $timeout = $this->getClientTimeout();
152        $extensionId = $resolver->getExtensionId();
153
154        $this->config = array_merge_recursive(
155            [
156                TokenService::JS_DATA_KEY => $this->getEncodedValue($this->tokenService->getClientConfig()),
157                'extensionsAliases' => $this->clientLibRegistry->getLibAliasMap(),
158                'libConfigs' => $this->getLibConfigs(),
159                'buster' => $this->assetService->getCacheBuster(),
160                'locale' => $langCode,
161                'client_timeout' => $timeout,
162                'crossorigin' => $this->isCrossorigin(),
163                'tao_base_www' => $taoBaseWww,
164                'context' => $this->getEncodedValue(
165                    [
166                        'tenantId' => $this->sessionService->getTenantId(),
167                        'root_url' => ROOT_URL,
168                        'base_url' => $this->getExtension($extensionId)->getConstant('BASE_URL'),
169                        'taobase_www' => $taoBaseWww,
170                        'base_www' => $this->assetService->getJsBaseWww($extensionId),
171                        'base_lang' => $this->getLang($langCode),
172                        'locale' => $langCode,
173                        'base_authoring_lang' => $this->userLanguageService->getAuthoringLanguage(),
174                        'timeout' => $timeout,
175                        'extension' => $extensionId,
176                        'module' => $resolver->getControllerShortName(),
177                        'action' => $resolver->getMethodName(),
178                        'shownExtension' => $this->getShownExtension($query),
179                        'shownStructure' => $this->getShownStructure($query),
180                        'bundle' => $this->modeHelper->isMode(tao_helpers_Mode::PRODUCTION),
181                        'featureFlags' => $this->featureFlagRepository->list(),
182                        'cookiePolicy' => $this->cookiePolicyConfigurationRetriever->retrieve()->jsonSerialize(),
183                        'currentUser' => $this->getUserData($currentSession),
184                    ]
185                ),
186            ],
187            $this->config
188        );
189
190        foreach ($this->clientConfigService->getExtendedConfig() as $key => $value) {
191            $this->config[$key] = $this->getEncodedValue($value);
192        }
193
194        return $this->config;
195    }
196
197    private function getUserData(common_session_Session $currentSession): array
198    {
199        $uri = $currentSession->getUserUri();
200        $id = $uri;
201        $login = $currentSession->getUserLabel();
202
203        foreach ($currentSession->getUser()->getPropertyValues(UserRdf::PROPERTY_LOGIN) as $rdfLogin) {
204            if (!empty($rdfLogin)) {
205                $login = $rdfLogin;
206            }
207        }
208
209        /** @var UserDataSessionContext $context */
210        foreach ($currentSession->getContexts(UserDataSessionContext::class) as $context) {
211            if ($context->getUserId()) {
212                $id = $context->getUserId();
213            }
214
215            if ($context->getUserLogin()) {
216                $login = $context->getUserLogin();
217            }
218        }
219
220        return [
221            'id' => $id,
222            'uri' => $uri,
223            'login' => $login,
224        ];
225    }
226
227    private function getClientTimeout(): int
228    {
229        $config = $this->getExtension('tao')->getConfig('js');
230
231        return (int) ($config['timeout'] ?? 30);
232    }
233
234    private function isCrossOrigin(): bool
235    {
236        $config = $this->getExtension('tao')->getConfig('js');
237
238        return (bool) ($config['crossorigin'] ?? false);
239    }
240
241    private function getExtension(string $extensionId): common_ext_Extension
242    {
243        try {
244            return $this->extensionsManager->getExtensionById($extensionId);
245        } catch (common_ext_ExtensionException $e) {
246            throw new Exception(__('Wrong parameter shownExtension'), $e);
247        }
248    }
249
250    private function getShownExtension(GetConfigQuery $command): ?string
251    {
252        $shownExtension = $command->getShownExtension();
253
254        return empty($shownExtension) ? null : $this->getExtension($shownExtension)->getName();
255    }
256
257    private function getShownStructure(GetConfigQuery $command): ?string
258    {
259        $shownStructure = $command->getShownStructure();
260
261        if ($shownStructure === null) {
262            return null;
263        }
264
265        foreach ($this->menuService->retrieveAllPerspectives() as $perspective) {
266            if ($perspective->getId() === $shownStructure) {
267                return $perspective->getId();
268            }
269        }
270
271        return null;
272    }
273
274    private function getLibConfigs(): array
275    {
276        $libConfigs = $this->featureFlagConfigSwitcher->getSwitchedClientConfig();
277
278        $libConfigs['util/locale']['dateTimeFormat'] = $this->dateFormatterFactory->create()->getJavascriptFormat(
279            tao_helpers_Date::FORMAT_LONG
280        );
281
282        return $libConfigs;
283    }
284
285    private function getLang(string $langCode): string
286    {
287        $langCodeDashPosition = strpos($langCode, '-');
288
289        return $langCodeDashPosition > 0
290            ? strtolower(substr($langCode, 0, $langCodeDashPosition))
291            : strtolower($langCode);
292    }
293
294    private function getEncodedValue(array $data): string
295    {
296        try {
297            return json_encode($data, JSON_THROW_ON_ERROR);
298        } catch (Throwable $exception) {
299            $this->logger->error('Cannot encode client config data: ' . $exception->getMessage());
300
301            return '';
302        }
303    }
304}