DependencyAnalyzer.php 12 KB

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