Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
91.11% |
123 / 135 |
|
61.54% |
8 / 13 |
CRAP | |
0.00% |
0 / 1 |
| version_helper | |
91.11% |
123 / 135 |
|
61.54% |
8 / 13 |
67.97 | |
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 | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
20 | |||
| 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, 30); |
| 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 | } |