[TASK] Update php-cs-fixer to 2.5.0
[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 }
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 return;
406 }
407
408 $isDownloadableVersionCompatible = $this->isDownloadableVersionCompatible($dependency);
409 if (!$isDownloadableVersionCompatible) {
410 if (!$this->skipDependencyCheck) {
411 throw new Exception\MissingVersionDependencyException(
412 'No compatible version found for extension ' . $extensionKey,
413 1399161284
414 );
415 }
416 return;
417 }
418
419 $latestCompatibleExtensionByIntegerVersionDependency = $this->getLatestCompatibleExtensionByIntegerVersionDependency($dependency);
420 if (!$latestCompatibleExtensionByIntegerVersionDependency instanceof Extension) {
421 if (!$this->skipDependencyCheck) {
422 throw new Exception\MissingExtensionDependencyException(
423 'Could not resolve dependency for "' . $dependency->getIdentifier() . '"',
424 1399161302
425 );
426 }
427 return;
428 }
429
430 if ($this->isDependentExtensionLoaded($extensionKey)) {
431 $this->managementService->markExtensionForUpdate($latestCompatibleExtensionByIntegerVersionDependency);
432 } else {
433 $this->managementService->markExtensionForDownload($latestCompatibleExtensionByIntegerVersionDependency);
434 }
435 }
436
437 /**
438 * @param string $extensionKey
439 * @return bool
440 */
441 protected function isDependentExtensionLoaded($extensionKey)
442 {
443 return ExtensionManagementUtility::isLoaded($extensionKey);
444 }
445
446 /**
447 * @param Dependency $dependency
448 * @return bool
449 */
450 protected function isLoadedVersionCompatible(Dependency $dependency)
451 {
452 $extensionVersion = ExtensionManagementUtility::getExtensionVersion($dependency->getIdentifier());
453 return $this->isVersionCompatible($extensionVersion, $dependency);
454 }
455
456 /**
457 * @param string $version
458 * @param Dependency $dependency
459 * @return bool
460 */
461 protected function isVersionCompatible($version, Dependency $dependency)
462 {
463 if (!($dependency->getLowestVersion() === '') && version_compare($version, $dependency->getLowestVersion()) === -1) {
464 return false;
465 }
466 if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), $version) === -1) {
467 return false;
468 }
469 return true;
470 }
471
472 /**
473 * Checks whether the needed extension is available
474 * (not necessarily installed, but present in system)
475 *
476 * @param string $extensionKey
477 * @return bool
478 */
479 protected function isDependentExtensionAvailable($extensionKey)
480 {
481 $this->setAvailableExtensions();
482 return array_key_exists($extensionKey, $this->availableExtensions);
483 }
484
485 /**
486 * Checks whether the available version is compatible
487 *
488 * @param Dependency $dependency
489 * @return bool
490 */
491 protected function isAvailableVersionCompatible(Dependency $dependency)
492 {
493 $this->setAvailableExtensions();
494 $extensionData = $this->emConfUtility->includeEmConf($this->availableExtensions[$dependency->getIdentifier()]);
495 return $this->isVersionCompatible($extensionData['version'], $dependency);
496 }
497
498 /**
499 * Checks whether a ter extension with $extensionKey exists
500 *
501 * @param string $extensionKey
502 * @return bool
503 */
504 protected function isExtensionDownloadableFromTer($extensionKey)
505 {
506 return $this->extensionRepository->countByExtensionKey($extensionKey) > 0;
507 }
508
509 /**
510 * Checks whether a compatible version of the extension exists in TER
511 *
512 * @param Dependency $dependency
513 * @return bool
514 */
515 protected function isDownloadableVersionCompatible(Dependency $dependency)
516 {
517 $versions = $this->getLowestAndHighestIntegerVersions($dependency);
518 $count = $this->extensionRepository->countByVersionRangeAndExtensionKey(
519 $dependency->getIdentifier(),
520 $versions['lowestIntegerVersion'],
521 $versions['highestIntegerVersion']
522 );
523 return !empty($count);
524 }
525
526 /**
527 * Get the latest compatible version of an extension that
528 * fulfills the given dependency from TER
529 *
530 * @param Dependency $dependency
531 * @return Extension
532 */
533 protected function getLatestCompatibleExtensionByIntegerVersionDependency(Dependency $dependency)
534 {
535 $versions = $this->getLowestAndHighestIntegerVersions($dependency);
536 $compatibleDataSets = $this->extensionRepository->findByVersionRangeAndExtensionKeyOrderedByVersion(
537 $dependency->getIdentifier(),
538 $versions['lowestIntegerVersion'],
539 $versions['highestIntegerVersion']
540 );
541 return $compatibleDataSets->getFirst();
542 }
543
544 /**
545 * Return array of lowest and highest version of dependency as integer
546 *
547 * @param Dependency $dependency
548 * @return array
549 */
550 protected function getLowestAndHighestIntegerVersions(Dependency $dependency)
551 {
552 $lowestVersion = $dependency->getLowestVersion();
553 $lowestVersionInteger = $lowestVersion ? VersionNumberUtility::convertVersionNumberToInteger($lowestVersion) : 0;
554 $highestVersion = $dependency->getHighestVersion();
555 $highestVersionInteger = $highestVersion ? VersionNumberUtility::convertVersionNumberToInteger($highestVersion) : 0;
556 return [
557 'lowestIntegerVersion' => $lowestVersionInteger,
558 'highestIntegerVersion' => $highestVersionInteger
559 ];
560 }
561
562 /**
563 * @param string $extensionKey
564 * @return array
565 */
566 public function findInstalledExtensionsThatDependOnMe($extensionKey)
567 {
568 $availableAndInstalledExtensions = $this->listUtility->getAvailableAndInstalledExtensionsWithAdditionalInformation();
569 $dependentExtensions = [];
570 foreach ($availableAndInstalledExtensions as $availableAndInstalledExtensionKey => $availableAndInstalledExtension) {
571 if (isset($availableAndInstalledExtension['installed']) && $availableAndInstalledExtension['installed'] === true) {
572 if (is_array($availableAndInstalledExtension['constraints']) && is_array($availableAndInstalledExtension['constraints']['depends']) && array_key_exists($extensionKey, $availableAndInstalledExtension['constraints']['depends'])) {
573 $dependentExtensions[] = $availableAndInstalledExtensionKey;
574 }
575 }
576 }
577 return $dependentExtensions;
578 }
579
580 /**
581 * Get extensions (out of a given list) that are suitable for the current TYPO3 version
582 *
583 * @param \TYPO3\CMS\Extbase\Persistence\QueryResultInterface|array $extensions List of extensions to check
584 * @return array List of extensions suitable for current TYPO3 version
585 */
586 public function getExtensionsSuitableForTypo3Version($extensions)
587 {
588 $suitableExtensions = [];
589 /** @var Extension $extension */
590 foreach ($extensions as $extension) {
591 /** @var Dependency $dependency */
592 foreach ($extension->getDependencies() as $dependency) {
593 if ($dependency->getIdentifier() === 'typo3') {
594 try {
595 if ($this->checkTypo3Dependency($dependency)) {
596 $suitableExtensions[] = $extension;
597 }
598 } catch (Exception\UnresolvedTypo3DependencyException $e) {
599 }
600 break;
601 }
602 }
603 }
604 return $suitableExtensions;
605 }
606
607 /**
608 * Gets a list of various extensions in various versions and returns
609 * a filtered list containing the extension-version combination with
610 * the highest version number.
611 *
612 * @param Extension[] $extensions
613 * @param bool $showUnsuitable
614 *
615 * @return \TYPO3\CMS\Extensionmanager\Domain\Model\Extension[]
616 */
617 public function filterYoungestVersionOfExtensionList(array $extensions, $showUnsuitable)
618 {
619 if (!$showUnsuitable) {
620 $extensions = $this->getExtensionsSuitableForTypo3Version($extensions);
621 }
622 $filteredExtensions = [];
623 foreach ($extensions as $extension) {
624 $extensionKey = $extension->getExtensionKey();
625 if (!array_key_exists($extensionKey, $filteredExtensions)) {
626 $filteredExtensions[$extensionKey] = $extension;
627 continue;
628 }
629 $currentVersion = $filteredExtensions[$extensionKey]->getVersion();
630 $newVersion = $extension->getVersion();
631 if (version_compare($newVersion, $currentVersion, '>')) {
632 $filteredExtensions[$extensionKey] = $extension;
633 }
634 }
635 return $filteredExtensions;
636 }
637 }