DependencyAnalyzer.php 12 KB

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