Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
62.50% covered (warning)
62.50%
45 / 72
18.18% covered (danger)
18.18%
2 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
CompatibilityChecker
62.50% covered (warning)
62.50%
45 / 72
18.18% covered (danger)
18.18%
2 / 11
109.19
0.00% covered (danger)
0.00%
0 / 1
 getCompatibilityList
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
5.67
 getSupportedList
15.38% covered (danger)
15.38%
2 / 13
0.00% covered (danger)
0.00%
0 / 1
8.45
 filterVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 versionCompare
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkVersion
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 isExcluded
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
6.03
 isBrowserExcluded
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 isOsExcluded
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 isCompatibleConfig
83.33% covered (warning)
83.33%
20 / 24
0.00% covered (danger)
0.00%
0 / 1
13.78
 getBrowserDetector
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOsDetector
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
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) 2015-2021 (original work) Open Assessment Technologies SA;
19 */
20
21namespace oat\taoClientDiagnostic\model;
22
23use common_exception_InconsistentData;
24use common_exception_FileSystemError;
25use common_exception_MissingParameter;
26use oat\oatbox\service\ConfigurableService;
27use oat\taoClientDiagnostic\model\exclusionList\ExcludedBrowserService;
28use oat\taoClientDiagnostic\model\exclusionList\ExcludedOSService;
29use oat\taoClientDiagnostic\model\SupportedList\SupportedListInterface;
30use Sinergi\BrowserDetector\Browser;
31use Sinergi\BrowserDetector\Os;
32
33class CompatibilityChecker extends ConfigurableService
34{
35    public const SERVICE_ID = 'taoClientDiagnostic/CompatibilityChecker';
36
37    public const COMPATIBILITY_NONE = 0;
38    public const COMPATIBILITY_COMPATIBLE = 1;
39    public const COMPATIBILITY_NOT_TESTED = 2;
40    public const COMPATIBILITY_SUPPORTED = 3;
41    public const COMPATIBILITY_NOT_SUPPORTED = 4;
42
43    protected $compatibility;
44    protected $supported;
45    protected $excludedBrowsers;
46    protected $excludedOS;
47
48    /**
49     * Extract compatibility file
50     * @throws common_exception_FileSystemError
51     */
52    public function getCompatibilityList(): array
53    {
54        if ($this->compatibility === null) {
55            $compatibilityFile = __DIR__ . '/../include/compatibility.json';
56
57            if (!file_exists($compatibilityFile)) {
58                throw new common_exception_FileSystemError('Unable to find the compatibility file');
59            }
60            $this->compatibility = json_decode(file_get_contents($compatibilityFile), true);
61        }
62        return $this->compatibility;
63    }
64
65    /**
66     * Fetch the support list
67     * @throws common_exception_FileSystemError
68     * @throws common_exception_InconsistentData
69     * @throws common_exception_MissingParameter
70     */
71    public function getSupportedList(): array
72    {
73        if ($this->supported == null) {
74            /** @var SupportedListInterface $remoteSupportedListService */
75            $remoteSupportedListService = $this->getServiceLocator()->get(SupportedListInterface::SERVICE_ID);
76            $supportedList = $remoteSupportedListService->getList();
77
78            if (!$supportedList) {
79                throw new common_exception_InconsistentData('Unable to decode list of supported browsers');
80            }
81
82            $this->supported = array_map(function ($entry) {
83                $entry['compatible'] = self::COMPATIBILITY_SUPPORTED;
84
85                $entry['versions'] = array_merge(...array_map(static function (string $version): array {
86                    return explode('-', $version);
87                }, $entry['versions']));
88
89                return $entry;
90            }, $supportedList);
91        }
92        return $this->supported;
93    }
94
95    protected function filterVersion($version): string
96    {
97        return preg_replace('#(\.0+)+($|-)#', '', $version);
98    }
99
100    /**
101     * Standard version_compare threats that  5.2 < 5.2.0, 5.2 < 5.2.1, ...
102     */
103    protected function versionCompare($ver1, $ver2): int
104    {
105        return version_compare($this->filterVersion($ver1), $this->filterVersion($ver2));
106    }
107
108    /**
109     * Check if a version is greater or equal to the listed ones
110     */
111    protected function checkVersion($testedVersion, $versionList): bool
112    {
113        if (empty($versionList)) {
114            return true;
115        }
116
117        foreach ($versionList as $version) {
118            if ($this->versionCompare($testedVersion, $version) >= 0) {
119                return true;
120            }
121        }
122
123        return false;
124    }
125
126    /**
127     * Checks if a version is excluded
128     */
129    protected function isExcluded($name, $version, $exclusionsList): bool
130    {
131        $name = strtolower($name);
132        if (count($exclusionsList) && array_key_exists($name, $exclusionsList)) {
133            $explodedVersion = explode('.', $version);
134            $excludedVersions = $exclusionsList[$name];
135            foreach ($excludedVersions as $excludedVersion) {
136                if (empty($excludedVersion)) {
137                    // any version is excluded
138                    return true;
139                }
140                $explodedExcludedVersion = explode('.', $excludedVersion);
141                if (array_slice($explodedVersion, 0, count($explodedExcludedVersion)) == $explodedExcludedVersion) {
142                    // greedy or exact version is excluded
143                    return true;
144                }
145            }
146        }
147        return false;
148    }
149
150    /**
151     * Checks if a browser is excluded
152     */
153    public function isBrowserExcluded($name, $version): bool
154    {
155        if ($this->excludedBrowsers === null) {
156            $service = $this->getServiceLocator()->get(ExcludedBrowserService::SERVICE_ID);
157            $this->excludedBrowsers = $service->getExclusionsList();
158        }
159        return $this->isExcluded($name, $version, $this->excludedBrowsers);
160    }
161
162    /**
163     * Checks if an OS is excluded
164     */
165    public function isOsExcluded($name, $version): bool
166    {
167        if ($this->excludedOS === null) {
168            $service = $this->getServiceLocator()->get(ExcludedOSService::SERVICE_ID);
169            $this->excludedOS = $service->getExclusionsList();
170        }
171        return $this->isExcluded($name, $version, $this->excludedOS);
172    }
173
174    /**
175     * Checks if the client browser, and the OS, meet the requirements supplied in a validation list.
176     * Returns a value corresponding to the COMPATIBILITY_* constants.
177     * @throws common_exception_FileSystemError
178     * @throws common_exception_InconsistentData
179     * @throws common_exception_MissingParameter
180     */
181    public function isCompatibleConfig(): int
182    {
183        $clientDevice = $this->getOsDetector()->isMobile() ? 'mobile' : 'desktop';
184        $clientOS = strtolower($this->getOsDetector()->getName());
185        $clientOSVersion = $this->getOsDetector()->getVersion();
186        $clientBrowser = strtolower($this->getBrowserDetector()->getName());
187        $clientBrowserVersion = $this->getBrowserDetector()->getVersion();
188
189        if (
190            $this->isOsExcluded($clientOS, $clientOSVersion) ||
191            $this->isBrowserExcluded($clientBrowser, $clientBrowserVersion)
192        ) {
193            return self::COMPATIBILITY_NOT_SUPPORTED;
194        }
195
196        $validationList = array_merge($this->getSupportedList(), $this->getCompatibilityList());
197
198        foreach ($validationList as $entry) {
199            if ($clientDevice !== $entry['device']) {
200                continue;
201            }
202
203            if ($entry['os']) {
204                if (strtolower($entry['os']) !== $clientOS) {
205                    continue;
206                }
207
208                if ($entry['osVersion'] && $this->versionCompare($clientOSVersion, $entry['osVersion']) !== 0) {
209                    continue;
210                }
211            }
212
213            if (strtolower($entry['browser']) !== $clientBrowser) {
214                continue;
215            }
216
217            if ($this->checkVersion($clientBrowserVersion, $entry['versions'])) {
218                if (isset($entry['compatible'])) {
219                    return $entry['compatible'];
220                }
221                return self::COMPATIBILITY_COMPATIBLE;
222            }
223        }
224
225        return self::COMPATIBILITY_NOT_TESTED;
226    }
227
228    /**
229     * Get the browser detector
230     */
231    protected function getBrowserDetector(): Browser
232    {
233        return new Browser();
234    }
235
236    /**
237     * Get the operating system detector
238     */
239    protected function getOsDetector(): Os
240    {
241        return new Os();
242    }
243}