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