1<?php 2/** 3 * @copyright Copyright (c) 2016, ownCloud, Inc. 4 * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> 5 * 6 * @author Bernhard Posselt <dev@bernhard-posselt.com> 7 * @author Christoph Wurst <christoph@winzerhof-wurst.at> 8 * @author Joas Schilling <coding@schilljs.com> 9 * @author Julius Härtl <jus@bitgrid.net> 10 * @author Lukas Reschke <lukas@statuscode.ch> 11 * @author Morris Jobke <hey@morrisjobke.de> 12 * @author Roeland Jago Douma <roeland@famdouma.nl> 13 * @author Stefan Weil <sw@weilnetz.de> 14 * @author Thomas Müller <thomas.mueller@tmit.eu> 15 * 16 * @license AGPL-3.0 17 * 18 * This code is free software: you can redistribute it and/or modify 19 * it under the terms of the GNU Affero General Public License, version 3, 20 * as published by the Free Software Foundation. 21 * 22 * This program is distributed in the hope that it will be useful, 23 * but WITHOUT ANY WARRANTY; without even the implied warranty of 24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 * GNU Affero General Public License for more details. 26 * 27 * You should have received a copy of the GNU Affero General Public License, version 3, 28 * along with this program. If not, see <http://www.gnu.org/licenses/> 29 * 30 */ 31namespace OC\App; 32 33use OCP\IL10N; 34 35class DependencyAnalyzer { 36 37 /** @var Platform */ 38 private $platform; 39 /** @var \OCP\IL10N */ 40 private $l; 41 /** @var array */ 42 private $appInfo; 43 44 /** 45 * @param Platform $platform 46 * @param \OCP\IL10N $l 47 */ 48 public function __construct(Platform $platform, IL10N $l) { 49 $this->platform = $platform; 50 $this->l = $l; 51 } 52 53 /** 54 * @param array $app 55 * @returns array of missing dependencies 56 */ 57 public function analyze(array $app, bool $ignoreMax = false) { 58 $this->appInfo = $app; 59 if (isset($app['dependencies'])) { 60 $dependencies = $app['dependencies']; 61 } else { 62 $dependencies = []; 63 } 64 65 return array_merge( 66 $this->analyzeArchitecture($dependencies), 67 $this->analyzePhpVersion($dependencies), 68 $this->analyzeDatabases($dependencies), 69 $this->analyzeCommands($dependencies), 70 $this->analyzeLibraries($dependencies), 71 $this->analyzeOS($dependencies), 72 $this->analyzeOC($dependencies, $app, $ignoreMax) 73 ); 74 } 75 76 public function isMarkedCompatible(array $app): bool { 77 if (isset($app['dependencies'])) { 78 $dependencies = $app['dependencies']; 79 } else { 80 $dependencies = []; 81 } 82 83 $maxVersion = $this->getMaxVersion($dependencies, $app); 84 if ($maxVersion === null) { 85 return true; 86 } 87 return !$this->compareBigger($this->platform->getOcVersion(), $maxVersion); 88 } 89 90 /** 91 * Truncates both versions to the lowest common version, e.g. 92 * 5.1.2.3 and 5.1 will be turned into 5.1 and 5.1, 93 * 5.2.6.5 and 5.1 will be turned into 5.2 and 5.1 94 * @param string $first 95 * @param string $second 96 * @return string[] first element is the first version, second element is the 97 * second version 98 */ 99 private function normalizeVersions($first, $second) { 100 $first = explode('.', $first); 101 $second = explode('.', $second); 102 103 // get both arrays to the same minimum size 104 $length = min(count($second), count($first)); 105 $first = array_slice($first, 0, $length); 106 $second = array_slice($second, 0, $length); 107 108 return [implode('.', $first), implode('.', $second)]; 109 } 110 111 /** 112 * Parameters will be normalized and then passed into version_compare 113 * in the same order they are specified in the method header 114 * @param string $first 115 * @param string $second 116 * @param string $operator 117 * @return bool result similar to version_compare 118 */ 119 private function compare($first, $second, $operator) { 120 // we can't normalize versions if one of the given parameters is not a 121 // version string but null. In case one parameter is null normalization 122 // will therefore be skipped 123 if ($first !== null && $second !== null) { 124 [$first, $second] = $this->normalizeVersions($first, $second); 125 } 126 127 return version_compare($first, $second, $operator); 128 } 129 130 /** 131 * Checks if a version is bigger than another version 132 * @param string $first 133 * @param string $second 134 * @return bool true if the first version is bigger than the second 135 */ 136 private function compareBigger($first, $second) { 137 return $this->compare($first, $second, '>'); 138 } 139 140 /** 141 * Checks if a version is smaller than another version 142 * @param string $first 143 * @param string $second 144 * @return bool true if the first version is smaller than the second 145 */ 146 private function compareSmaller($first, $second) { 147 return $this->compare($first, $second, '<'); 148 } 149 150 /** 151 * @param array $dependencies 152 * @return array 153 */ 154 private function analyzePhpVersion(array $dependencies) { 155 $missing = []; 156 if (isset($dependencies['php']['@attributes']['min-version'])) { 157 $minVersion = $dependencies['php']['@attributes']['min-version']; 158 if ($this->compareSmaller($this->platform->getPhpVersion(), $minVersion)) { 159 $missing[] = $this->l->t('PHP %s or higher is required.', [$minVersion]); 160 } 161 } 162 if (isset($dependencies['php']['@attributes']['max-version'])) { 163 $maxVersion = $dependencies['php']['@attributes']['max-version']; 164 if ($this->compareBigger($this->platform->getPhpVersion(), $maxVersion)) { 165 $missing[] = $this->l->t('PHP with a version lower than %s is required.', [$maxVersion]); 166 } 167 } 168 if (isset($dependencies['php']['@attributes']['min-int-size'])) { 169 $intSize = $dependencies['php']['@attributes']['min-int-size']; 170 if ($intSize > $this->platform->getIntSize() * 8) { 171 $missing[] = $this->l->t('%sbit or higher PHP required.', [$intSize]); 172 } 173 } 174 return $missing; 175 } 176 177 private function analyzeArchitecture(array $dependencies) { 178 $missing = []; 179 if (!isset($dependencies['architecture'])) { 180 return $missing; 181 } 182 183 $supportedArchitectures = $dependencies['architecture']; 184 if (empty($supportedArchitectures)) { 185 return $missing; 186 } 187 if (!is_array($supportedArchitectures)) { 188 $supportedArchitectures = [$supportedArchitectures]; 189 } 190 $supportedArchitectures = array_map(function ($architecture) { 191 return $this->getValue($architecture); 192 }, $supportedArchitectures); 193 $currentArchitecture = $this->platform->getArchitecture(); 194 if (!in_array($currentArchitecture, $supportedArchitectures, true)) { 195 $missing[] = $this->l->t('The following architectures are supported: %s', [implode(', ', $supportedArchitectures)]); 196 } 197 return $missing; 198 } 199 200 /** 201 * @param array $dependencies 202 * @return array 203 */ 204 private function analyzeDatabases(array $dependencies) { 205 $missing = []; 206 if (!isset($dependencies['database'])) { 207 return $missing; 208 } 209 210 $supportedDatabases = $dependencies['database']; 211 if (empty($supportedDatabases)) { 212 return $missing; 213 } 214 if (!is_array($supportedDatabases)) { 215 $supportedDatabases = [$supportedDatabases]; 216 } 217 $supportedDatabases = array_map(function ($db) { 218 return $this->getValue($db); 219 }, $supportedDatabases); 220 $currentDatabase = $this->platform->getDatabase(); 221 if (!in_array($currentDatabase, $supportedDatabases)) { 222 $missing[] = $this->l->t('The following databases are supported: %s', [implode(', ', $supportedDatabases)]); 223 } 224 return $missing; 225 } 226 227 /** 228 * @param array $dependencies 229 * @return array 230 */ 231 private function analyzeCommands(array $dependencies) { 232 $missing = []; 233 if (!isset($dependencies['command'])) { 234 return $missing; 235 } 236 237 $commands = $dependencies['command']; 238 if (!is_array($commands)) { 239 $commands = [$commands]; 240 } 241 if (isset($commands['@value'])) { 242 $commands = [$commands]; 243 } 244 $os = $this->platform->getOS(); 245 foreach ($commands as $command) { 246 if (isset($command['@attributes']['os']) && $command['@attributes']['os'] !== $os) { 247 continue; 248 } 249 $commandName = $this->getValue($command); 250 if (!$this->platform->isCommandKnown($commandName)) { 251 $missing[] = $this->l->t('The command line tool %s could not be found', [$commandName]); 252 } 253 } 254 return $missing; 255 } 256 257 /** 258 * @param array $dependencies 259 * @return array 260 */ 261 private function analyzeLibraries(array $dependencies) { 262 $missing = []; 263 if (!isset($dependencies['lib'])) { 264 return $missing; 265 } 266 267 $libs = $dependencies['lib']; 268 if (!is_array($libs)) { 269 $libs = [$libs]; 270 } 271 if (isset($libs['@value'])) { 272 $libs = [$libs]; 273 } 274 foreach ($libs as $lib) { 275 $libName = $this->getValue($lib); 276 $libVersion = $this->platform->getLibraryVersion($libName); 277 if (is_null($libVersion)) { 278 $missing[] = $this->l->t('The library %s is not available.', [$libName]); 279 continue; 280 } 281 282 if (is_array($lib)) { 283 if (isset($lib['@attributes']['min-version'])) { 284 $minVersion = $lib['@attributes']['min-version']; 285 if ($this->compareSmaller($libVersion, $minVersion)) { 286 $missing[] = $this->l->t('Library %1$s with a version higher than %2$s is required - available version %3$s.', 287 [$libName, $minVersion, $libVersion]); 288 } 289 } 290 if (isset($lib['@attributes']['max-version'])) { 291 $maxVersion = $lib['@attributes']['max-version']; 292 if ($this->compareBigger($libVersion, $maxVersion)) { 293 $missing[] = $this->l->t('Library %1$s with a version lower than %2$s is required - available version %3$s.', 294 [$libName, $maxVersion, $libVersion]); 295 } 296 } 297 } 298 } 299 return $missing; 300 } 301 302 /** 303 * @param array $dependencies 304 * @return array 305 */ 306 private function analyzeOS(array $dependencies) { 307 $missing = []; 308 if (!isset($dependencies['os'])) { 309 return $missing; 310 } 311 312 $oss = $dependencies['os']; 313 if (empty($oss)) { 314 return $missing; 315 } 316 if (is_array($oss)) { 317 $oss = array_map(function ($os) { 318 return $this->getValue($os); 319 }, $oss); 320 } else { 321 $oss = [$oss]; 322 } 323 $currentOS = $this->platform->getOS(); 324 if (!in_array($currentOS, $oss)) { 325 $missing[] = $this->l->t('The following platforms are supported: %s', [implode(', ', $oss)]); 326 } 327 return $missing; 328 } 329 330 /** 331 * @param array $dependencies 332 * @param array $appInfo 333 * @return array 334 */ 335 private function analyzeOC(array $dependencies, array $appInfo, bool $ignoreMax) { 336 $missing = []; 337 $minVersion = null; 338 if (isset($dependencies['nextcloud']['@attributes']['min-version'])) { 339 $minVersion = $dependencies['nextcloud']['@attributes']['min-version']; 340 } elseif (isset($dependencies['owncloud']['@attributes']['min-version'])) { 341 $minVersion = $dependencies['owncloud']['@attributes']['min-version']; 342 } elseif (isset($appInfo['requiremin'])) { 343 $minVersion = $appInfo['requiremin']; 344 } elseif (isset($appInfo['require'])) { 345 $minVersion = $appInfo['require']; 346 } 347 $maxVersion = $this->getMaxVersion($dependencies, $appInfo); 348 349 if (!is_null($minVersion)) { 350 if ($this->compareSmaller($this->platform->getOcVersion(), $minVersion)) { 351 $missing[] = $this->l->t('Server version %s or higher is required.', [$this->toVisibleVersion($minVersion)]); 352 } 353 } 354 if (!$ignoreMax && !is_null($maxVersion)) { 355 if ($this->compareBigger($this->platform->getOcVersion(), $maxVersion)) { 356 $missing[] = $this->l->t('Server version %s or lower is required.', [$this->toVisibleVersion($maxVersion)]); 357 } 358 } 359 return $missing; 360 } 361 362 private function getMaxVersion(array $dependencies, array $appInfo): ?string { 363 if (isset($dependencies['nextcloud']['@attributes']['max-version'])) { 364 return $dependencies['nextcloud']['@attributes']['max-version']; 365 } 366 if (isset($dependencies['owncloud']['@attributes']['max-version'])) { 367 return $dependencies['owncloud']['@attributes']['max-version']; 368 } 369 if (isset($appInfo['requiremax'])) { 370 return $appInfo['requiremax']; 371 } 372 373 return null; 374 } 375 376 /** 377 * Map the internal version number to the Nextcloud version 378 * 379 * @param string $version 380 * @return string 381 */ 382 protected function toVisibleVersion($version) { 383 switch ($version) { 384 case '9.1': 385 return '10'; 386 default: 387 if (strpos($version, '9.1.') === 0) { 388 $version = '10.0.' . substr($version, 4); 389 } 390 return $version; 391 } 392 } 393 394 /** 395 * @param $element 396 * @return mixed 397 */ 398 private function getValue($element) { 399 if (isset($element['@value'])) { 400 return $element['@value']; 401 } 402 return (string)$element; 403 } 404} 405