9ca546620cd9141c14d677ec8a3fc321983f0e53
[Packages/TYPO3.CMS.git] / typo3 / sysext / extensionmanager / Classes / Utility / DependencyUtility.php
1 <?php
2 namespace TYPO3\CMS\Extensionmanager\Utility;
3
4 /**
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
17 use TYPO3\CMS\Core\Utility\VersionNumberUtility;
18 use TYPO3\CMS\Extensionmanager\Domain\Model\Dependency;
19 use TYPO3\CMS\Extensionmanager\Domain\Model\Extension;
20 use TYPO3\CMS\Extensionmanager\Exception;
21
22 /**
23 * Utility for dealing with dependencies
24 *
25 * @author Susanne Moog <susanne.moog@typo3.org>
26 */
27 class DependencyUtility implements \TYPO3\CMS\Core\SingletonInterface {
28
29 /**
30 * @var \TYPO3\CMS\Extbase\Object\ObjectManager
31 * @inject
32 */
33 protected $objectManager;
34
35 /**
36 * @var \TYPO3\CMS\Extensionmanager\Domain\Repository\ExtensionRepository
37 * @inject
38 */
39 protected $extensionRepository;
40
41 /**
42 * @var \TYPO3\CMS\Extensionmanager\Utility\ListUtility
43 * @inject
44 */
45 protected $listUtility;
46
47 /**
48 * @var \TYPO3\CMS\Extensionmanager\Utility\EmConfUtility
49 * @inject
50 */
51 protected $emConfUtility;
52
53 /**
54 * @var \TYPO3\CMS\Extensionmanager\Service\ExtensionManagementService
55 * @inject
56 */
57 protected $managementService;
58
59 /**
60 * @var array
61 */
62 protected $availableExtensions = array();
63
64 /**
65 * @var string
66 */
67 protected $localExtensionStorage = '';
68
69 /**
70 * @var array
71 */
72 protected $dependencyErrors = array();
73
74 /**
75 * @var bool
76 */
77 protected $skipSystemDependencyCheck = FALSE;
78
79 /**
80 * @param string $localExtensionStorage
81 * @return void
82 */
83 public function setLocalExtensionStorage($localExtensionStorage) {
84 $this->localExtensionStorage = $localExtensionStorage;
85 }
86
87 /**
88 * Setter for available extensions
89 * gets available extensions from list utility if not already done
90 *
91 * @return void
92 */
93 protected function setAvailableExtensions() {
94 $this->availableExtensions = $this->listUtility->getAvailableExtensions();
95 }
96
97 /**
98 * @param bool $skipSpecialDependencyCheck
99 * @return void
100 */
101 public function setSkipSystemDependencyCheck($skipSpecialDependencyCheck) {
102 $this->skipSystemDependencyCheck = $skipSpecialDependencyCheck;
103 }
104
105 /**
106 * Checks dependencies for special cases (currently typo3 and php)
107 *
108 * @param Extension $extension
109 * @return void
110 */
111 public function checkDependencies(Extension $extension) {
112 $dependencies = $extension->getDependencies();
113 foreach ($dependencies as $dependency) {
114 /** @var Dependency $dependency */
115 $identifier = strtolower($dependency->getIdentifier());
116 try {
117 if (in_array($identifier, Dependency::$specialDependencies)) {
118 if (!$this->skipSystemDependencyCheck) {
119 $methodName = 'check' . ucfirst($identifier) . 'Dependency';
120 $this->{$methodName}($dependency);
121 }
122 } else {
123 if ($dependency->getType() === 'depends') {
124 $this->checkExtensionDependency($dependency);
125 }
126 }
127 } catch (Exception\UnresolvedDependencyException $e) {
128 if (in_array($identifier, Dependency::$specialDependencies)) {
129 $extensionKey = $extension->getExtensionKey();
130 } else {
131 $extensionKey = $identifier;
132 }
133 if (!isset($this->dependencyErrors[$extensionKey])) {
134 $this->dependencyErrors[$extensionKey] = array();
135 }
136 $this->dependencyErrors[$extensionKey][] = array(
137 'code' => $e->getCode(),
138 'message' => $e->getMessage()
139 );
140 }
141 }
142 }
143
144 /**
145 * Returns TRUE if a dependency error was found
146 *
147 * @return bool
148 */
149 public function hasDependencyErrors() {
150 return !empty($this->dependencyErrors);
151 }
152
153 /**
154 * Return the dependency errors
155 *
156 * @return array
157 */
158 public function getDependencyErrors() {
159 return $this->dependencyErrors;
160 }
161
162 /**
163 * Returns true if current TYPO3 version fulfills extension requirements
164 *
165 * @param Dependency $dependency
166 * @throws Exception\UnresolvedTypo3DependencyException
167 * @return boolean
168 */
169 protected function checkTypo3Dependency(Dependency $dependency) {
170 $lowerCaseIdentifier = strtolower($dependency->getIdentifier());
171 if ($lowerCaseIdentifier === 'typo3') {
172 if (!($dependency->getLowestVersion() === '') && version_compare(VersionNumberUtility::getNumericTypo3Version(), $dependency->getLowestVersion()) === -1) {
173 throw new Exception\UnresolvedTypo3DependencyException(
174 'Your TYPO3 version is lower than necessary. You need at least TYPO3 version ' . $dependency->getLowestVersion(),
175 1399144499
176 );
177 }
178 if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), VersionNumberUtility::getNumericTypo3Version()) === -1) {
179 throw new Exception\UnresolvedTypo3DependencyException(
180 'Your TYPO3 version is higher than allowed. You can use TYPO3 versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
181 1399144521
182 );
183 }
184 } else {
185 throw new Exception\UnresolvedTypo3DependencyException(
186 'checkTypo3Dependency can only check TYPO3 dependencies. Found dependency with identifier "' . $dependency->getIdentifier() . '"',
187 1399144551
188 );
189 }
190 return TRUE;
191 }
192
193 /**
194 * Returns true if current php version fulfills extension requirements
195 *
196 * @param Dependency $dependency
197 * @throws Exception\UnresolvedPhpDependencyException
198 * @return boolean
199 */
200 protected function checkPhpDependency(Dependency $dependency) {
201 $lowerCaseIdentifier = strtolower($dependency->getIdentifier());
202 if ($lowerCaseIdentifier === 'php') {
203 if (!($dependency->getLowestVersion() === '') && version_compare(PHP_VERSION, $dependency->getLowestVersion()) === -1) {
204 throw new Exception\UnresolvedPhpDependencyException(
205 'Your PHP version is lower than necessary. You need at least PHP version ' . $dependency->getLowestVersion(),
206 1377977857
207 );
208 }
209 if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), PHP_VERSION) === -1) {
210 throw new Exception\UnresolvedPhpDependencyException(
211 'Your PHP version is higher than allowed. You can use PHP versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
212 1377977856
213 );
214 }
215 } else {
216 throw new Exception\UnresolvedPhpDependencyException(
217 'checkPhpDependency can only check PHP dependencies. Found dependency with identifier "' . $dependency->getIdentifier() . '"',
218 1377977858
219 );
220 }
221 return TRUE;
222 }
223
224 /**
225 * Main controlling function for checking dependencies
226 * Dependency check is done in the following way:
227 * - installed extension in matching version ? - return true
228 * - available extension in matching version ? - mark for installation
229 * - remote (TER) extension in matching version? - mark for download
230 *
231 * @todo handle exceptions / markForUpload
232 * @param Dependency $dependency
233 * @throws Exception\MissingVersionDependencyException
234 * @return boolean
235 */
236 protected function checkExtensionDependency(Dependency $dependency) {
237 $extensionKey = $dependency->getIdentifier();
238 $extensionIsLoaded = $this->isDependentExtensionLoaded($extensionKey);
239 if ($extensionIsLoaded === TRUE) {
240 $isLoadedVersionCompatible = $this->isLoadedVersionCompatible($dependency);
241 if ($isLoadedVersionCompatible === TRUE) {
242 return TRUE;
243 } else {
244 try {
245 $this->getExtensionFromRepository($extensionKey, $dependency);
246 } catch (Exception\UnresolvedDependencyException $e) {
247 throw new Exception\MissingVersionDependencyException(
248 'The extension ' . $dependency->getIdentifier() . ' is needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion() . ', but could not be fetched from TER',
249 1396302624
250 );
251 }
252 }
253 } else {
254 $extensionIsAvailable = $this->isDependentExtensionAvailable($extensionKey);
255 if ($extensionIsAvailable === TRUE) {
256 $isAvailableVersionCompatible = $this->isAvailableVersionCompatible($dependency);
257 if ($isAvailableVersionCompatible) {
258 $this->managementService->markExtensionForInstallation($extensionKey);
259 } else {
260 $this->getExtensionFromRepository($extensionKey, $dependency);
261 }
262 } else {
263 $this->getExtensionFromRepository($extensionKey, $dependency);
264 }
265 }
266 return FALSE;
267 }
268
269 /**
270 * Get an extension from a repository
271 * (might be in the extension itself or the TER)
272 *
273 * @param string $extensionKey
274 * @param Dependency $dependency
275 * @return void
276 */
277 protected function getExtensionFromRepository($extensionKey, Dependency $dependency) {
278 if (!$this->getExtensionFromInExtensionRepository($extensionKey, $dependency)) {
279 $this->getExtensionFromTer($extensionKey, $dependency);
280 }
281 }
282
283 /**
284 * Gets an extension from the in extension repository
285 * (the local extension storage)
286 *
287 * @param string $extensionKey
288 * @return boolean
289 */
290 protected function getExtensionFromInExtensionRepository($extensionKey) {
291 if ($this->localExtensionStorage !== '' && is_dir($this->localExtensionStorage)) {
292 $extList = \TYPO3\CMS\Core\Utility\GeneralUtility::get_dirs($this->localExtensionStorage);
293 if (in_array($extensionKey, $extList)) {
294 $this->managementService->markExtensionForCopy($extensionKey, $this->localExtensionStorage);
295 return TRUE;
296 }
297 }
298 return FALSE;
299 }
300
301 /**
302 * Handles checks to find a compatible extension version from TER to fulfill given dependency
303 *
304 * @todo unit tests
305 * @param string $extensionKey
306 * @param Dependency $dependency
307 * @throws Exception\UnresolvedDependencyException
308 * @return void
309 */
310 protected function getExtensionFromTer($extensionKey, Dependency $dependency) {
311 $isExtensionDownloadableFromTer = $this->isExtensionDownloadableFromTer($extensionKey);
312 if (!$isExtensionDownloadableFromTer) {
313 if (!$this->skipSystemDependencyCheck) {
314 throw new Exception\MissingExtensionDependencyException(
315 'The extension ' . $extensionKey . ' is not available from TER.',
316 1399161266
317 );
318 }
319 return;
320 }
321
322 $isDownloadableVersionCompatible = $this->isDownloadableVersionCompatible($dependency);
323 if (!$isDownloadableVersionCompatible) {
324 if (!$this->skipSystemDependencyCheck) {
325 throw new Exception\MissingVersionDependencyException(
326 'No compatible version found for extension ' . $extensionKey,
327 1399161284
328 );
329 }
330 return;
331 }
332
333 $latestCompatibleExtensionByIntegerVersionDependency = $this->getLatestCompatibleExtensionByIntegerVersionDependency($dependency);
334 if (!$latestCompatibleExtensionByIntegerVersionDependency instanceof Extension) {
335 if (!$this->skipSystemDependencyCheck) {
336 throw new Exception\MissingExtensionDependencyException(
337 'Could not resolve dependency for "' . $dependency->getIdentifier() . '"',
338 1399161302
339 );
340 }
341 return;
342 }
343
344 if ($this->isDependentExtensionLoaded($extensionKey)) {
345 $this->managementService->markExtensionForUpdate($latestCompatibleExtensionByIntegerVersionDependency);
346 } else {
347 $this->managementService->markExtensionForDownload($latestCompatibleExtensionByIntegerVersionDependency);
348 }
349 }
350
351 /**
352 * @param string $extensionKey
353 * @return boolean
354 */
355 protected function isDependentExtensionLoaded($extensionKey) {
356 return ExtensionManagementUtility::isLoaded($extensionKey);
357 }
358
359 /**
360 * @param Dependency $dependency
361 * @return boolean
362 */
363 protected function isLoadedVersionCompatible(Dependency $dependency) {
364 $extensionVersion = ExtensionManagementUtility::getExtensionVersion($dependency->getIdentifier());
365 return $this->isVersionCompatible($extensionVersion, $dependency);
366 }
367
368 /**
369 * @param string $version
370 * @param Dependency $dependency
371 * @return boolean
372 */
373 protected function isVersionCompatible($version, Dependency $dependency) {
374 if (!($dependency->getLowestVersion() === '') && version_compare($version, $dependency->getLowestVersion()) === -1) {
375 return FALSE;
376 }
377 if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), $version) === -1) {
378 return FALSE;
379 }
380 return TRUE;
381 }
382
383 /**
384 * Checks whether the needed extension is available
385 * (not necessarily installed, but present in system)
386 *
387 * @param string $extensionKey
388 * @return boolean
389 */
390 protected function isDependentExtensionAvailable($extensionKey) {
391 $this->setAvailableExtensions();
392 return array_key_exists($extensionKey, $this->availableExtensions);
393 }
394
395 /**
396 * Checks whether the available version is compatible
397 *
398 * @param Dependency $dependency
399 * @return boolean
400 */
401 protected function isAvailableVersionCompatible(Dependency $dependency) {
402 $this->setAvailableExtensions();
403 $extensionData = $this->emConfUtility->includeEmConf($this->availableExtensions[$dependency->getIdentifier()]);
404 return $this->isVersionCompatible($extensionData['version'], $dependency);
405 }
406
407 /**
408 * Checks whether a ter extension with $extensionKey exists
409 *
410 * @param string $extensionKey
411 * @return boolean
412 */
413 protected function isExtensionDownloadableFromTer($extensionKey) {
414 return $this->extensionRepository->countByExtensionKey($extensionKey) > 0;
415 }
416
417 /**
418 * Checks whether a compatible version of the extension exists in TER
419 *
420 * @param Dependency $dependency
421 * @return boolean
422 */
423 protected function isDownloadableVersionCompatible(Dependency $dependency) {
424 $versions = $this->getLowestAndHighestIntegerVersions($dependency);
425 $count = $this->extensionRepository->countByVersionRangeAndExtensionKey(
426 $dependency->getIdentifier(), $versions['lowestIntegerVersion'], $versions['highestIntegerVersion']
427 );
428 return !empty($count);
429 }
430
431 /**
432 * Get the latest compatible version of an extension that
433 * fulfills the given dependency from TER
434 *
435 * @param Dependency $dependency
436 * @return Extension
437 */
438 protected function getLatestCompatibleExtensionByIntegerVersionDependency(Dependency $dependency) {
439 $versions = $this->getLowestAndHighestIntegerVersions($dependency);
440 $compatibleDataSets = $this->extensionRepository->findByVersionRangeAndExtensionKeyOrderedByVersion(
441 $dependency->getIdentifier(),
442 $versions['lowestIntegerVersion'],
443 $versions['highestIntegerVersion']
444 );
445 return $compatibleDataSets->getFirst();
446 }
447
448 /**
449 * Return array of lowest and highest version of dependency as integer
450 *
451 * @param Dependency $dependency
452 * @return array
453 */
454 protected function getLowestAndHighestIntegerVersions(Dependency $dependency) {
455 $lowestVersion = $dependency->getLowestVersion();
456 $lowestVersionInteger = $lowestVersion ? VersionNumberUtility::convertVersionNumberToInteger($lowestVersion) : 0;
457 $highestVersion = $dependency->getHighestVersion();
458 $highestVersionInteger = $highestVersion ? VersionNumberUtility::convertVersionNumberToInteger($highestVersion) : 0;
459 return array(
460 'lowestIntegerVersion' => $lowestVersionInteger,
461 'highestIntegerVersion' => $highestVersionInteger
462 );
463 }
464
465 public function findInstalledExtensionsThatDependOnMe($extensionKey) {
466 $availableAndInstalledExtensions = $this->listUtility->getAvailableAndInstalledExtensionsWithAdditionalInformation();
467 $dependentExtensions = array();
468 foreach ($availableAndInstalledExtensions as $availableAndInstalledExtensionKey => $availableAndInstalledExtension) {
469 if (isset($availableAndInstalledExtension['installed']) && $availableAndInstalledExtension['installed'] === TRUE) {
470 if (is_array($availableAndInstalledExtension['constraints']) && is_array($availableAndInstalledExtension['constraints']['depends']) && array_key_exists($extensionKey, $availableAndInstalledExtension['constraints']['depends'])) {
471 $dependentExtensions[] = $availableAndInstalledExtensionKey;
472 }
473 }
474 }
475 return $dependentExtensions;
476 }
477
478 }