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