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