You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DependencyAnalyzer.php 12KB

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