Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
93.33% |
126 / 135 |
|
61.54% |
8 / 13 |
CRAP | |
0.00% |
0 / 1 |
version_helper | |
93.33% |
126 / 135 |
|
61.54% |
8 / 13 |
66.25 | |
0.00% |
0 / 1 |
__construct | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
set_file_location | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
set_current_version | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
force_stability | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
compare | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
is_stable | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
get_latest_on_current_branch | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
get_update_on_branch | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
8 | |||
get_ext_update_on_branch | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
9 | |||
get_suggested_updates | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
get_versions_matching_stability | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
4.25 | |||
get_versions | |
88.24% |
15 / 17 |
|
0.00% |
0 / 1 |
11.20 | |||
validate_versions | |
90.00% |
36 / 40 |
|
0.00% |
0 / 1 |
21.44 |
1 | <?php |
2 | /** |
3 | * |
4 | * This file is part of the phpBB Forum Software package. |
5 | * |
6 | * @copyright (c) phpBB Limited <https://www.phpbb.com> |
7 | * @license GNU General Public License, version 2 (GPL-2.0) |
8 | * |
9 | * For full copyright and license information, please see |
10 | * the docs/CREDITS.txt file. |
11 | * |
12 | */ |
13 | |
14 | namespace phpbb; |
15 | |
16 | use phpbb\exception\version_check_exception; |
17 | use phpbb\json\sanitizer as json_sanitizer; |
18 | |
19 | /** |
20 | * Class to handle version checking and comparison |
21 | */ |
22 | class version_helper |
23 | { |
24 | /** |
25 | * @var string Host |
26 | */ |
27 | protected $host = 'version.phpbb.com'; |
28 | |
29 | /** |
30 | * @var string Path to file |
31 | */ |
32 | protected $path = '/phpbb'; |
33 | |
34 | /** |
35 | * @var string File name |
36 | */ |
37 | protected $file = 'versions.json'; |
38 | |
39 | /** |
40 | * @var bool Use SSL or not |
41 | */ |
42 | protected $use_ssl = true; |
43 | |
44 | /** |
45 | * @var string Current version installed |
46 | */ |
47 | protected $current_version; |
48 | |
49 | /** |
50 | * @var null|string Null to not force stability, 'unstable' or 'stable' to |
51 | * force the corresponding stability |
52 | */ |
53 | protected $force_stability; |
54 | |
55 | /** @var \phpbb\cache\service */ |
56 | protected $cache; |
57 | |
58 | /** @var \phpbb\config\config */ |
59 | protected $config; |
60 | |
61 | /** @var \phpbb\file_downloader */ |
62 | protected $file_downloader; |
63 | |
64 | protected $version_schema = array( |
65 | 'stable' => array( |
66 | 'current' => 'version', |
67 | 'download' => 'url', |
68 | 'announcement' => 'url', |
69 | 'eol' => 'url', |
70 | 'security' => 'bool', |
71 | ), |
72 | 'unstable' => array( |
73 | 'current' => 'version', |
74 | 'download' => 'url', |
75 | 'announcement' => 'url', |
76 | 'eol' => 'url', |
77 | 'security' => 'bool', |
78 | ), |
79 | ); |
80 | |
81 | /** |
82 | * Constructor |
83 | * |
84 | * @param \phpbb\cache\service $cache |
85 | * @param \phpbb\config\config $config |
86 | * @param \phpbb\file_downloader $file_downloader |
87 | */ |
88 | public function __construct(\phpbb\cache\service $cache, \phpbb\config\config $config, \phpbb\file_downloader $file_downloader) |
89 | { |
90 | $this->cache = $cache; |
91 | $this->config = $config; |
92 | $this->file_downloader = $file_downloader; |
93 | |
94 | if (defined('PHPBB_QA')) |
95 | { |
96 | $this->force_stability = 'unstable'; |
97 | } |
98 | |
99 | $this->current_version = $this->config['version']; |
100 | } |
101 | |
102 | /** |
103 | * Set location to the file |
104 | * |
105 | * @param string $host Host (e.g. version.phpbb.com) |
106 | * @param string $path Path to file (e.g. /phpbb) |
107 | * @param string $file File name (Default: versions.json) |
108 | * @param bool $use_ssl Use SSL or not (Default: false) |
109 | * @return version_helper |
110 | */ |
111 | public function set_file_location($host, $path, $file = 'versions.json', $use_ssl = false) |
112 | { |
113 | $this->host = $host; |
114 | $this->path = $path; |
115 | $this->file = $file; |
116 | $this->use_ssl = $use_ssl; |
117 | |
118 | return $this; |
119 | } |
120 | |
121 | /** |
122 | * Set current version |
123 | * |
124 | * @param string $version The current version |
125 | * @return version_helper |
126 | */ |
127 | public function set_current_version($version) |
128 | { |
129 | $this->current_version = $version; |
130 | |
131 | return $this; |
132 | } |
133 | |
134 | /** |
135 | * Over-ride the stability to force check to include unstable versions |
136 | * |
137 | * @param null|string $stability Null to not force stability, 'unstable' or 'stable' to |
138 | * force the corresponding stability |
139 | * @return version_helper |
140 | */ |
141 | public function force_stability($stability) |
142 | { |
143 | $this->force_stability = $stability; |
144 | |
145 | return $this; |
146 | } |
147 | |
148 | /** |
149 | * Wrapper for version_compare() that allows using uppercase A and B |
150 | * for alpha and beta releases. |
151 | * |
152 | * See http://www.php.net/manual/en/function.version-compare.php |
153 | * |
154 | * @param string $version1 First version number |
155 | * @param string $version2 Second version number |
156 | * @param string $operator Comparison operator (optional) |
157 | * |
158 | * @return mixed Boolean (true, false) if comparison operator is specified. |
159 | * Integer (-1, 0, 1) otherwise. |
160 | */ |
161 | public function compare($version1, $version2, $operator = null) |
162 | { |
163 | return phpbb_version_compare($version1, $version2, $operator); |
164 | } |
165 | |
166 | /** |
167 | * Check whether or not a version is "stable" |
168 | * |
169 | * Stable means only numbers OR a pl release |
170 | * |
171 | * @param string $version |
172 | * @return bool Bool true or false |
173 | */ |
174 | public function is_stable($version) |
175 | { |
176 | $matches = false; |
177 | preg_match('/^[\d\.]+/', $version, $matches); |
178 | |
179 | if (empty($matches[0])) |
180 | { |
181 | return false; |
182 | } |
183 | |
184 | return $this->compare($version, $matches[0], '>='); |
185 | } |
186 | |
187 | /** |
188 | * Gets the latest version for the current branch the user is on |
189 | * |
190 | * @param bool $force_update Ignores cached data. Defaults to false. |
191 | * @param bool $force_cache Force the use of the cache. Override $force_update. |
192 | * @return string |
193 | * @throws version_check_exception |
194 | */ |
195 | public function get_latest_on_current_branch($force_update = false, $force_cache = false) |
196 | { |
197 | $versions = $this->get_versions_matching_stability($force_update, $force_cache); |
198 | |
199 | $self = $this; |
200 | $current_version = $this->current_version; |
201 | |
202 | // Filter out any versions less than the current version |
203 | $versions = array_filter($versions, function($data) use ($self, $current_version) { |
204 | return $self->compare($data['current'], $current_version, '>='); |
205 | }); |
206 | |
207 | // Get the lowest version from the previous list. |
208 | return array_reduce($versions, function($value, $data) use ($self) { |
209 | if ($value === null || $self->compare($data['current'], $value, '<')) |
210 | { |
211 | return $data['current']; |
212 | } |
213 | |
214 | return $value; |
215 | }); |
216 | } |
217 | |
218 | /** |
219 | * Gets the latest update for the current branch the user is on |
220 | * Will suggest versions from newer branches when EoL has been reached |
221 | * and/or version from newer branch is needed for having all known security |
222 | * issues fixed. |
223 | * |
224 | * @param bool $force_update Ignores cached data. Defaults to false. |
225 | * @param bool $force_cache Force the use of the cache. Override $force_update. |
226 | * @return array Version info or empty array if there are no updates |
227 | * @throws \RuntimeException |
228 | */ |
229 | public function get_update_on_branch($force_update = false, $force_cache = false) |
230 | { |
231 | $versions = $this->get_versions_matching_stability($force_update, $force_cache); |
232 | |
233 | $self = $this; |
234 | $current_version = $this->current_version; |
235 | |
236 | // Filter out any versions less than the current version |
237 | $versions = array_filter($versions, function($data) use ($self, $current_version) { |
238 | return $self->compare($data['current'], $current_version, '>='); |
239 | }); |
240 | |
241 | // Get the lowest version from the previous list. |
242 | $update_info = array_reduce($versions, function($value, $data) use ($self, $current_version) { |
243 | if ($value === null && $self->compare($data['current'], $current_version, '>=')) |
244 | { |
245 | if (!$data['eol'] && (!$data['security'] || $self->compare($data['security'], $data['current'], '<='))) |
246 | { |
247 | return ($self->compare($data['current'], $current_version, '>')) ? $data : array(); |
248 | } |
249 | else |
250 | { |
251 | return null; |
252 | } |
253 | } |
254 | |
255 | return $value; |
256 | }); |
257 | |
258 | return $update_info === null ? array() : $update_info; |
259 | } |
260 | |
261 | /** |
262 | * Gets the latest extension update for the current phpBB branch the user is on |
263 | * Will suggest versions from newer branches when EoL has been reached |
264 | * and/or version from newer branch is needed for having all known security |
265 | * issues fixed. |
266 | * |
267 | * @param bool $force_update Ignores cached data. Defaults to false. |
268 | * @param bool $force_cache Force the use of the cache. Override $force_update. |
269 | * @return array Version info or empty array if there are no updates |
270 | * @throws \RuntimeException |
271 | */ |
272 | public function get_ext_update_on_branch($force_update = false, $force_cache = false) |
273 | { |
274 | $versions = $this->get_versions_matching_stability($force_update, $force_cache); |
275 | |
276 | $self = $this; |
277 | $current_version = $this->current_version; |
278 | |
279 | // Get current phpBB branch from version, e.g.: 3.2 |
280 | preg_match('/^(\d+\.\d+).*$/', $this->config['version'], $matches); |
281 | $current_branch = $matches[1]; |
282 | |
283 | // Filter out any versions less than the current version |
284 | $versions = array_filter($versions, function($data) use ($self, $current_version) { |
285 | return $self->compare($data['current'], $current_version, '>='); |
286 | }); |
287 | |
288 | // Filter out any phpbb branches less than the current version |
289 | $branches = array_filter(array_keys($versions), function($branch) use ($self, $current_branch) { |
290 | return $self->compare($branch, $current_branch, '>='); |
291 | }); |
292 | if (!empty($branches)) |
293 | { |
294 | $versions = array_intersect_key($versions, array_flip($branches)); |
295 | } |
296 | else |
297 | { |
298 | // If branches are empty, it means the current phpBB branch is newer than any branch the |
299 | // extension was validated against. Reverse sort the versions array so we get the newest |
300 | // validated release available. |
301 | krsort($versions); |
302 | } |
303 | |
304 | // Get the first available version from the previous list. |
305 | $update_info = array_reduce($versions, function($value, $data) use ($self, $current_version) { |
306 | if ($value === null && $self->compare($data['current'], $current_version, '>=')) |
307 | { |
308 | if (!$data['eol'] && (!$data['security'] || $self->compare($data['security'], $data['current'], '<='))) |
309 | { |
310 | return $self->compare($data['current'], $current_version, '>') ? $data : array(); |
311 | } |
312 | else |
313 | { |
314 | return null; |
315 | } |
316 | } |
317 | |
318 | return $value; |
319 | }); |
320 | |
321 | return $update_info === null ? array() : $update_info; |
322 | } |
323 | |
324 | /** |
325 | * Obtains the latest version information |
326 | * |
327 | * @param bool $force_update Ignores cached data. Defaults to false. |
328 | * @param bool $force_cache Force the use of the cache. Override $force_update. |
329 | * @return array |
330 | * @throws version_check_exception |
331 | */ |
332 | public function get_suggested_updates($force_update = false, $force_cache = false) |
333 | { |
334 | $versions = $this->get_versions_matching_stability($force_update, $force_cache); |
335 | |
336 | $self = $this; |
337 | $current_version = $this->current_version; |
338 | |
339 | // Filter out any versions less than or equal to the current version |
340 | return array_filter($versions, function($data) use ($self, $current_version) { |
341 | return $self->compare($data['current'], $current_version, '>'); |
342 | }); |
343 | } |
344 | |
345 | /** |
346 | * Obtains the latest version information matching the stability of the current install |
347 | * |
348 | * @param bool $force_update Ignores cached data. Defaults to false. |
349 | * @param bool $force_cache Force the use of the cache. Override $force_update. |
350 | * @return array Version info |
351 | * @throws version_check_exception |
352 | */ |
353 | public function get_versions_matching_stability($force_update = false, $force_cache = false) |
354 | { |
355 | $info = $this->get_versions($force_update, $force_cache); |
356 | |
357 | if ($this->force_stability !== null) |
358 | { |
359 | return ($this->force_stability === 'unstable') ? $info['unstable'] : $info['stable']; |
360 | } |
361 | |
362 | return ($this->is_stable($this->current_version)) ? $info['stable'] : $info['unstable']; |
363 | } |
364 | |
365 | /** |
366 | * Obtains the latest version information |
367 | * |
368 | * @param bool $force_update Ignores cached data. Defaults to false. |
369 | * @param bool $force_cache Force the use of the cache. Override $force_update. |
370 | * @return array Version info, includes stable and unstable data |
371 | * @throws version_check_exception |
372 | */ |
373 | public function get_versions($force_update = false, $force_cache = false) |
374 | { |
375 | $cache_file = '_versioncheck_' . $this->host . $this->path . $this->file . $this->use_ssl; |
376 | |
377 | $info = $this->cache->get($cache_file); |
378 | |
379 | if ($info === false && $force_cache) |
380 | { |
381 | throw new version_check_exception('VERSIONCHECK_FAIL'); |
382 | } |
383 | else if ($info === false || $force_update) |
384 | { |
385 | $info = $this->file_downloader->get($this->host, $this->path, $this->file, $this->use_ssl ? 443 : 80); |
386 | $error_string = $this->file_downloader->get_error_string(); |
387 | |
388 | if (!empty($error_string)) |
389 | { |
390 | throw new version_check_exception($error_string); |
391 | } |
392 | |
393 | // Sanitize any data we retrieve from a server |
394 | $info = json_sanitizer::decode($info); |
395 | |
396 | if (empty($info['stable']) && empty($info['unstable'])) |
397 | { |
398 | throw new version_check_exception('VERSIONCHECK_FAIL'); |
399 | } |
400 | |
401 | $info['stable'] = (empty($info['stable'])) ? array() : $info['stable']; |
402 | $info['unstable'] = (empty($info['unstable'])) ? $info['stable'] : $info['unstable']; |
403 | |
404 | $info = $this->validate_versions($info); |
405 | |
406 | $this->cache->put($cache_file, $info, 86400); // 24 hours |
407 | } |
408 | |
409 | return $info; |
410 | } |
411 | |
412 | /** |
413 | * Validate versions info input |
414 | * |
415 | * @param array $versions_info Decoded json data array. Will be modified |
416 | * and cleaned by this method |
417 | * |
418 | * @return array Versions info array |
419 | * @throws version_check_exception |
420 | */ |
421 | public function validate_versions($versions_info) |
422 | { |
423 | $array_diff = array_diff_key($versions_info, array($this->version_schema)); |
424 | |
425 | // Remove excessive data |
426 | if (count($array_diff) > 0) |
427 | { |
428 | $old_versions_info = $versions_info; |
429 | $versions_info = array( |
430 | 'stable' => !empty($old_versions_info['stable']) ? $old_versions_info['stable'] : array(), |
431 | 'unstable' => !empty($old_versions_info['unstable']) ? $old_versions_info['unstable'] : array(), |
432 | ); |
433 | unset($old_versions_info); |
434 | } |
435 | |
436 | foreach ($versions_info as $stability_type => &$versions_data) |
437 | { |
438 | foreach ($versions_data as $branch => &$version_data) |
439 | { |
440 | if (!preg_match('/^[0-9a-z\-\.]+$/i', $branch)) |
441 | { |
442 | unset($versions_data[$branch]); |
443 | continue; |
444 | } |
445 | |
446 | $stability_diff = array_diff_key($version_data, $this->version_schema[$stability_type]); |
447 | |
448 | if (count($stability_diff) > 0) |
449 | { |
450 | $old_version_data = $version_data; |
451 | $version_data = array(); |
452 | foreach ($this->version_schema[$stability_type] as $key => $value) |
453 | { |
454 | if (isset($old_version_data[$key])) |
455 | { |
456 | $version_data[$key] = $old_version_data[$key]; |
457 | } |
458 | } |
459 | unset($old_version_data); |
460 | } |
461 | |
462 | foreach ($version_data as $key => &$value) |
463 | { |
464 | if (!isset($this->version_schema[$stability_type][$key])) |
465 | { |
466 | unset($version_data[$key]); |
467 | throw new version_check_exception('VERSIONCHECK_INVALID_ENTRY'); |
468 | } |
469 | |
470 | switch ($this->version_schema[$stability_type][$key]) |
471 | { |
472 | case 'bool': |
473 | $value = (bool) $value; |
474 | break; |
475 | |
476 | case 'url': |
477 | if (!empty($value) && !preg_match('#^' . get_preg_expression('url') . '$#iu', $value) && |
478 | !preg_match('#^' . get_preg_expression('www_url') . '$#iu', $value)) |
479 | { |
480 | throw new version_check_exception('VERSIONCHECK_INVALID_URL'); |
481 | } |
482 | break; |
483 | |
484 | case 'version': |
485 | if (!empty($value) && !preg_match(get_preg_expression('semantic_version'), $value)) |
486 | { |
487 | throw new version_check_exception('VERSIONCHECK_INVALID_VERSION'); |
488 | } |
489 | break; |
490 | |
491 | default: |
492 | // Shouldn't be possible to trigger this |
493 | throw new version_check_exception('VERSIONCHECK_INVALID_ENTRY'); |
494 | } |
495 | } |
496 | } |
497 | } |
498 | |
499 | return $versions_info; |
500 | } |
501 | } |