1<?php
2/**
3 * @author Bernhard Posselt <dev@bernhard-posselt.com>
4 * @author Joas Schilling <coding@schilljs.com>
5 * @author Lukas Reschke <lukas@statuscode.ch>
6 * @author Morris Jobke <hey@morrisjobke.de>
7 * @author Stefan Weil <sw@weilnetz.de>
8 * @author Thomas Müller <thomas.mueller@tmit.eu>
9 *
10 * @copyright Copyright (c) 2018, ownCloud GmbH
11 * @license AGPL-3.0
12 *
13 * This code is free software: you can redistribute it and/or modify
14 * it under the terms of the GNU Affero General Public License, version 3,
15 * as published by the Free Software Foundation.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU Affero General Public License for more details.
21 *
22 * You should have received a copy of the GNU Affero General Public License, version 3,
23 * along with this program.  If not, see <http://www.gnu.org/licenses/>
24 *
25 */
26
27namespace OC\App;
28
29use OCP\IL10N;
30
31class DependencyAnalyzer {
32
33	/** @var Platform */
34	private $platform;
35	/** @var \OCP\IL10N */
36	private $l;
37	/** @var array */
38	private $appInfo;
39
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	/**
50	 * @param array $app
51	 * @return array of missing dependencies
52	 */
53	public function analyze(array $app) {
54		$this->appInfo = $app;
55		if (isset($app['dependencies'])) {
56			$dependencies = $app['dependencies'];
57		} else {
58			$dependencies = [];
59		}
60
61		return \array_merge(
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)
68		);
69	}
70
71	/**
72	 * Truncates both versions to the lowest common version, e.g.
73	 * 5.1.2.3 and 5.1 will be turned into 5.1 and 5.1,
74	 * 5.2.6.5 and 5.1 will be turned into 5.2 and 5.1
75	 * @param string $first
76	 * @param string $second
77	 * @return string[] first element is the first version, second element is the
78	 * second version
79	 */
80	private function normalizeVersions($first, $second) {
81		$first = \explode('.', $first);
82		$second = \explode('.', $second);
83
84		// get both arrays to the same minimum size
85		$length = \min(\count($second), \count($first));
86		$first = \array_slice($first, 0, $length);
87		$second = \array_slice($second, 0, $length);
88
89		return [\implode('.', $first), \implode('.', $second)];
90	}
91
92	/**
93	 * Parameters will be normalized and then passed into version_compare
94	 * in the same order they are specified in the method header
95	 * @param string $first
96	 * @param string $second
97	 * @param string $operator
98	 * @return bool result similar to version_compare
99	 */
100	private function compare($first, $second, $operator) {
101		// we can't normalize versions if one of the given parameters is not a
102		// version string but null. In case one parameter is null normalization
103		// will therefore be skipped
104		if ($first !== null && $second !== null) {
105			list($first, $second) = $this->normalizeVersions($first, $second);
106		}
107
108		return \version_compare($first, $second, $operator);
109	}
110
111	/**
112	 * Checks if a version is bigger than another version
113	 * @param string $first
114	 * @param string $second
115	 * @return bool true if the first version is bigger than the second
116	 */
117	private function compareBigger($first, $second) {
118		return $this->compare($first, $second, '>');
119	}
120
121	/**
122	 * Checks if a version is smaller than another version
123	 * @param string $first
124	 * @param string $second
125	 * @return bool true if the first version is smaller than the second
126	 */
127	private function compareSmaller($first, $second) {
128		return $this->compare($first, $second, '<');
129	}
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[] = (string)$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[] = (string)$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[] = (string)$this->l->t('%sbit or higher PHP required.', $intSize);
153			}
154		}
155		return $missing;
156	}
157
158	/**
159	 * @param array $dependencies
160	 * @return array
161	 */
162	private function analyzeDatabases(array $dependencies) {
163		$missing = [];
164		if (!isset($dependencies['database'])) {
165			return $missing;
166		}
167
168		$supportedDatabases = $dependencies['database'];
169		if (empty($supportedDatabases)) {
170			return $missing;
171		}
172		if (!\is_array($supportedDatabases)) {
173			$supportedDatabases = [$supportedDatabases];
174		}
175		$supportedDatabases = \array_map(function ($db) {
176			return $this->getValue($db);
177		}, $supportedDatabases);
178		$currentDatabase = $this->platform->getDatabase();
179		if (!\in_array($currentDatabase, $supportedDatabases)) {
180			$missing[] = (string)$this->l->t('Following databases are supported: %s', \join(', ', $supportedDatabases));
181		}
182		return $missing;
183	}
184
185	/**
186	 * @param array $dependencies
187	 * @return array
188	 */
189	private function analyzeCommands(array $dependencies) {
190		$missing = [];
191		if (!isset($dependencies['command'])) {
192			return $missing;
193		}
194
195		$commands = $dependencies['command'];
196		if (!\is_array($commands)) {
197			$commands = [$commands];
198		}
199		if (isset($commands['@value'])) {
200			$commands = [$commands];
201		}
202		$os = $this->platform->getOS();
203		foreach ($commands as $command) {
204			if (isset($command['@attributes']['os']) && $command['@attributes']['os'] !== $os) {
205				continue;
206			}
207			$commandName = $this->getValue($command);
208			if (!$this->platform->isCommandKnown($commandName)) {
209				$missing[] = (string)$this->l->t('The command line tool %s could not be found', $commandName);
210			}
211		}
212		return $missing;
213	}
214
215	/**
216	 * @param array $dependencies
217	 * @return array
218	 */
219	private function analyzeLibraries(array $dependencies) {
220		$missing = [];
221		if (!isset($dependencies['lib'])) {
222			return $missing;
223		}
224
225		$libs = $dependencies['lib'];
226		if (!\is_array($libs)) {
227			$libs = [$libs];
228		}
229		if (isset($libs['@value'])) {
230			$libs = [$libs];
231		}
232		foreach ($libs as $lib) {
233			$libName = $this->getValue($lib);
234			$libVersion = $this->platform->getLibraryVersion($libName);
235			if ($libVersion === null) {
236				$missing[] = (string)$this->l->t('The library %s is not available.', $libName);
237				continue;
238			}
239
240			if (\is_array($lib)) {
241				if (isset($lib['@attributes']['min-version'])) {
242					$minVersion = $lib['@attributes']['min-version'];
243					if ($this->compareSmaller($libVersion, $minVersion)) {
244						$missing[] = (string)$this->l->t(
245							'Library %s with a version higher than %s is required - available version %s.',
246							[$libName, $minVersion, $libVersion]
247						);
248					}
249				}
250				if (isset($lib['@attributes']['max-version'])) {
251					$maxVersion = $lib['@attributes']['max-version'];
252					if ($this->compareBigger($libVersion, $maxVersion)) {
253						$missing[] = (string)$this->l->t(
254							'Library %s with a version lower than %s is required - available version %s.',
255							[$libName, $maxVersion, $libVersion]
256						);
257					}
258				}
259			}
260		}
261		return $missing;
262	}
263
264	/**
265	 * @param array $dependencies
266	 * @return array
267	 */
268	private function analyzeOS(array $dependencies) {
269		$missing = [];
270		if (!isset($dependencies['os'])) {
271			return $missing;
272		}
273
274		$oss = $dependencies['os'];
275		if (empty($oss)) {
276			return $missing;
277		}
278		if (\is_array($oss)) {
279			$oss = \array_map(function ($os) {
280				return $this->getValue($os);
281			}, $oss);
282		} else {
283			$oss = [$oss];
284		}
285		$currentOS = $this->platform->getOS();
286		if (!\in_array($currentOS, $oss)) {
287			$missing[] = (string)$this->l->t('Following platforms are supported: %s', \join(', ', $oss));
288		}
289		return $missing;
290	}
291
292	/**
293	 * @param array $dependencies
294	 * @param array $appInfo
295	 * @return array
296	 */
297	private function analyzeOC(array $dependencies, array $appInfo) {
298		$missing = [];
299		$minVersion = null;
300		if (isset($dependencies['owncloud']['@attributes']['min-version'])) {
301			$minVersion = $dependencies['owncloud']['@attributes']['min-version'];
302		} elseif (isset($appInfo['requiremin'])) {
303			$minVersion = $appInfo['requiremin'];
304		} elseif (isset($appInfo['require'])) {
305			$minVersion = $appInfo['require'];
306		}
307		$maxVersion = null;
308		if (isset($dependencies['owncloud']['@attributes']['max-version'])) {
309			$maxVersion = $dependencies['owncloud']['@attributes']['max-version'];
310		} elseif (isset($appInfo['requiremax'])) {
311			$maxVersion = $appInfo['requiremax'];
312		}
313
314		if ($minVersion !== null) {
315			if ($this->compareSmaller($this->platform->getOcVersion(), $minVersion)) {
316				$missing[] = (string)$this->l->t('ownCloud %s or higher is required.', $minVersion);
317			}
318		}
319		if ($maxVersion !== null) {
320			if ($this->compareBigger($this->platform->getOcVersion(), $maxVersion)) {
321				$missing[] = (string)$this->l->t('ownCloud %s or lower is required.', $maxVersion);
322			}
323		}
324		return $missing;
325	}
326
327	/**
328	 * @param $element
329	 * @return mixed
330	 */
331	private function getValue($element) {
332		if (isset($element['@value'])) {
333			return $element['@value'];
334		}
335		return (string)$element;
336	}
337}
338