Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.91% covered (success)
90.91%
80 / 88
63.64% covered (warning)
63.64%
7 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ClientConfigStorage
90.91% covered (success)
90.91%
80 / 88
63.64% covered (warning)
63.64%
7 / 11
19.27
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
14 / 14
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%
44 / 44
100.00% covered (success)
100.00%
1 / 1
2
 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 Exception;
31use oat\oatbox\session\SessionService;
32use oat\oatbox\user\UserLanguageServiceInterface;
33use oat\tao\helpers\dateFormatter\DateFormatterFactory;
34use oat\tao\model\asset\AssetService;
35use oat\tao\model\ClientLibRegistry;
36use oat\tao\model\featureFlag\FeatureFlagConfigSwitcher;
37use oat\tao\model\featureFlag\Repository\FeatureFlagRepositoryInterface;
38use oat\tao\model\menu\MenuService;
39use oat\tao\model\routing\ResolverFactory;
40use oat\tao\model\security\xsrf\TokenService;
41use Psr\Log\LoggerInterface;
42use tao_helpers_Date;
43use tao_helpers_Mode;
44use Throwable;
45
46class ClientConfigStorage
47{
48    private TokenService $tokenService;
49    private ClientLibRegistry $clientLibRegistry;
50    private FeatureFlagConfigSwitcher $featureFlagConfigSwitcher;
51    private AssetService $assetService;
52    private common_ext_ExtensionsManager $extensionsManager;
53    private ClientConfigService $clientConfigService;
54    private UserLanguageServiceInterface $userLanguageService;
55    private FeatureFlagRepositoryInterface $featureFlagRepository;
56    private ResolverFactory $resolverFactory;
57    private LoggerInterface $logger;
58    private SessionService $sessionService;
59    private tao_helpers_Mode $modeHelper;
60    private DateFormatterFactory $dateFormatterFactory;
61    private MenuService $menuService;
62
63    private array $config = [];
64
65    public function __construct(
66        TokenService $tokenService,
67        ClientLibRegistry $clientLibRegistry,
68        FeatureFlagConfigSwitcher $featureFlagConfigSwitcher,
69        AssetService $assetService,
70        common_ext_ExtensionsManager $extensionsManager,
71        ClientConfigService $clientConfigService,
72        UserLanguageServiceInterface $userLanguageService,
73        FeatureFlagRepositoryInterface $featureFlagRepository,
74        ResolverFactory $resolverFactory,
75        LoggerInterface $logger,
76        SessionService $sessionService,
77        tao_helpers_Mode $modeHelper,
78        DateFormatterFactory $dateFormatterFactory,
79        MenuService $menuService
80    ) {
81        $this->tokenService = $tokenService;
82        $this->clientLibRegistry = $clientLibRegistry;
83        $this->featureFlagConfigSwitcher = $featureFlagConfigSwitcher;
84        $this->assetService = $assetService;
85        $this->extensionsManager = $extensionsManager;
86        $this->clientConfigService = $clientConfigService;
87        $this->userLanguageService = $userLanguageService;
88        $this->featureFlagRepository = $featureFlagRepository;
89        $this->resolverFactory = $resolverFactory;
90        $this->logger = $logger;
91        $this->sessionService = $sessionService;
92        $this->modeHelper = $modeHelper;
93        $this->dateFormatterFactory = $dateFormatterFactory;
94        $this->menuService = $menuService;
95    }
96
97    /**
98     * Using the DI container you can set configs by providing config path and it's values using environment variables
99     * and ect.
100     *
101     * $services
102     *     ->get(ClientConfigStorage::class)
103     *     ->call(
104     *         'setConfigByPath',
105     *         [
106     *             [
107     *                 'libConfigs' => [
108     *                     'somePath' => [
109     *                         'someProp' => env('SOME_ENV_VARIABLE')->string(),
110     *                     ],
111     *                 ],
112     *             ],
113     *         ]
114     *     )
115     *     ->call(
116     *         'setConfigByPath',
117     *         [
118     *             [
119     *                 'context' => [
120     *                     'somePath' => [
121     *                         'someProp' => env('ANOTHER_ENV_VARIABLE')->int(),
122     *                     ],
123     *                 ],
124     *             ],
125     *         ]
126     *     );
127     */
128    public function setConfigByPath(array $path): void
129    {
130        $this->config = array_merge_recursive($this->config, $path);
131    }
132
133    public function getConfig(GetConfigQuery $query): array
134    {
135        $resolver = $this->resolverFactory->create([
136            'extension' => $query->getExtension(),
137            'action' => $query->getAction(),
138            'module' => $query->getModule(),
139        ]);
140
141        $taoBaseWww = $this->assetService->getJsBaseWww('tao');
142        $langCode = $this->sessionService->getCurrentSession()->getInterfaceLanguage();
143        $timeout = $this->getClientTimeout();
144        $extensionId = $resolver->getExtensionId();
145
146        $this->config = array_merge_recursive(
147            [
148                TokenService::JS_DATA_KEY => $this->getEncodedValue($this->tokenService->getClientConfig()),
149                'extensionsAliases' => $this->clientLibRegistry->getLibAliasMap(),
150                'libConfigs' => $this->getLibConfigs(),
151                'buster' => $this->assetService->getCacheBuster(),
152                'locale' => $langCode,
153                'client_timeout' => $timeout,
154                'crossorigin' => $this->isCrossorigin(),
155                'tao_base_www' => $taoBaseWww,
156                'context' => $this->getEncodedValue(
157                    [
158                        'root_url' => ROOT_URL,
159                        'base_url' => $this->getExtension($extensionId)->getConstant('BASE_URL'),
160                        'taobase_www' => $taoBaseWww,
161                        'base_www' => $this->assetService->getJsBaseWww($extensionId),
162                        'base_lang' => $this->getLang($langCode),
163                        'locale' => $langCode,
164                        'base_authoring_lang' => $this->userLanguageService->getAuthoringLanguage(),
165                        'timeout' => $timeout,
166                        'extension' => $extensionId,
167                        'module' => $resolver->getControllerShortName(),
168                        'action' => $resolver->getMethodName(),
169                        'shownExtension' => $this->getShownExtension($query),
170                        'shownStructure' => $this->getShownStructure($query),
171                        'bundle' => $this->modeHelper->isMode(tao_helpers_Mode::PRODUCTION),
172                        'featureFlags' => $this->featureFlagRepository->list(),
173                    ]
174                ),
175            ],
176            $this->config
177        );
178
179        foreach ($this->clientConfigService->getExtendedConfig() as $key => $value) {
180            $this->config[$key] = $this->getEncodedValue($value);
181        }
182
183        return $this->config;
184    }
185
186    private function getClientTimeout(): int
187    {
188        $config = $this->getExtension('tao')->getConfig('js');
189
190        return (int) ($config['timeout'] ?? 30);
191    }
192
193    private function isCrossOrigin(): bool
194    {
195        $config = $this->getExtension('tao')->getConfig('js');
196
197        return (bool) ($config['crossorigin'] ?? false);
198    }
199
200    private function getExtension(string $extensionId): common_ext_Extension
201    {
202        try {
203            return $this->extensionsManager->getExtensionById($extensionId);
204        } catch (common_ext_ExtensionException $e) {
205            throw new Exception(__('Wrong parameter shownExtension'), $e);
206        }
207    }
208
209    private function getShownExtension(GetConfigQuery $command): ?string
210    {
211        $shownExtension = $command->getShownExtension();
212
213        return empty($shownExtension) ? null : $this->getExtension($shownExtension)->getName();
214    }
215
216    private function getShownStructure(GetConfigQuery $command): ?string
217    {
218        $shownStructure = $command->getShownStructure();
219
220        if ($shownStructure === null) {
221            return null;
222        }
223
224        foreach ($this->menuService->retrieveAllPerspectives() as $perspective) {
225            if ($perspective->getId() === $shownStructure) {
226                return $perspective->getId();
227            }
228        }
229
230        return null;
231    }
232
233    private function getLibConfigs(): array
234    {
235        $libConfigs = $this->featureFlagConfigSwitcher->getSwitchedClientConfig();
236
237        $libConfigs['util/locale']['dateTimeFormat'] = $this->dateFormatterFactory->create()->getJavascriptFormat(
238            tao_helpers_Date::FORMAT_LONG
239        );
240
241        return $libConfigs;
242    }
243
244    private function getLang(string $langCode): string
245    {
246        $langCodeDashPosition = strpos($langCode, '-');
247
248        return $langCodeDashPosition > 0
249            ? strtolower(substr($langCode, 0, $langCodeDashPosition))
250            : strtolower($langCode);
251    }
252
253    private function getEncodedValue(array $data): string
254    {
255        try {
256            return json_encode($data, JSON_THROW_ON_ERROR);
257        } catch (Throwable $exception) {
258            $this->logger->error('Cannot encode client config data: ' . $exception->getMessage());
259
260            return '';
261        }
262    }
263}