DependencyAnalyzer.php 11 KB

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