[BUGFIX] Skip dependency check for extensions
[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 $skipDependencyCheck = 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 setSkipDependencyCheck($skipDependencyCheck) {
103 $this->skipDependencyCheck = $skipDependencyCheck;
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->skipDependencyCheck) {
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 || $this->skipDependencyCheck) {
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 if (!$this->skipDependencyCheck) {
281 throw new Exception\MissingVersionDependencyException(
282 'The extension ' . $extensionKey . ' is available in version ' . $availableVersion
283 . ' but is needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion() . ' and could not be fetched from TER',
284 1430560390
285 );
286 }
287 }
288 } else {
289 if (!$this->skipDependencyCheck) {
290 throw new Exception\MissingVersionDependencyException(
291 'The extension ' . $extensionKey . ' is available in version ' . $availableVersion
292 . ' but is needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
293 1430562374
294 );
295 }
296 }
297 }
298 } else {
299 $unresolvedDependencyErrors = $this->dependencyErrors;
300 $this->getExtensionFromRepository($extensionKey, $dependency);
301 $this->dependencyErrors = array_merge($unresolvedDependencyErrors, $this->dependencyErrors);
302 }
303 }
304
305 return FALSE;
306 }
307
308 /**
309 * Get an extension from a repository
310 * (might be in the extension itself or the TER)
311 *
312 * @param string $extensionKey
313 * @param Dependency $dependency
314 * @return void
315 * @throws Exception\UnresolvedDependencyException
316 */
317 protected function getExtensionFromRepository($extensionKey, Dependency $dependency) {
318 if (!$this->getExtensionFromInExtensionRepository($extensionKey, $dependency)) {
319 $this->getExtensionFromTer($extensionKey, $dependency);
320 }
321 }
322
323 /**
324 * Gets an extension from the in extension repository
325 * (the local extension storage)
326 *
327 * @param string $extensionKey
328 * @return bool
329 */
330 protected function getExtensionFromInExtensionRepository($extensionKey) {
331 if ($this->localExtensionStorage !== '' && is_dir($this->localExtensionStorage)) {
332 $extList = \TYPO3\CMS\Core\Utility\GeneralUtility::get_dirs($this->localExtensionStorage);
333 if (in_array($extensionKey, $extList)) {
334 $this->managementService->markExtensionForCopy($extensionKey, $this->localExtensionStorage);
335 return TRUE;
336 }
337 }
338 return FALSE;
339 }
340
341 /**
342 * Handles checks to find a compatible extension version from TER to fulfill given dependency
343 *
344 * @todo unit tests
345 * @param string $extensionKey
346 * @param Dependency $dependency
347 * @throws Exception\UnresolvedDependencyException
348 * @return void
349 */
350 protected function getExtensionFromTer($extensionKey, Dependency $dependency) {
351 $isExtensionDownloadableFromTer = $this->isExtensionDownloadableFromTer($extensionKey);
352 if (!$isExtensionDownloadableFromTer) {
353 if (!$this->skipDependencyCheck) {
354 if ($this->extensionRepository->countAll() > 0) {
355 throw new Exception\MissingExtensionDependencyException(
356 'The extension ' . $extensionKey . ' is not available from TER.',
357 1399161266
358 );
359 } else {
360 throw new Exception\MissingExtensionDependencyException(
361 'The extension ' . $extensionKey . ' could not be checked. Please update your Extension-List from TYPO3 Extension Repository (TER).',
362 1430580308
363 );
364 }
365 }
366 return;
367 }
368
369 $isDownloadableVersionCompatible = $this->isDownloadableVersionCompatible($dependency);
370 if (!$isDownloadableVersionCompatible) {
371 if (!$this->skipDependencyCheck) {
372 throw new Exception\MissingVersionDependencyException(
373 'No compatible version found for extension ' . $extensionKey,
374 1399161284
375 );
376 }
377 return;
378 }
379
380 $latestCompatibleExtensionByIntegerVersionDependency = $this->getLatestCompatibleExtensionByIntegerVersionDependency($dependency);
381 if (!$latestCompatibleExtensionByIntegerVersionDependency instanceof Extension) {
382 if (!$this->skipDependencyCheck) {
383 throw new Exception\MissingExtensionDependencyException(
384 'Could not resolve dependency for "' . $dependency->getIdentifier() . '"',
385 1399161302
386 );
387 }
388 return;
389 }
390
391 if ($this->isDependentExtensionLoaded($extensionKey)) {
392 $this->managementService->markExtensionForUpdate($latestCompatibleExtensionByIntegerVersionDependency);
393 } else {
394 $this->managementService->markExtensionForDownload($latestCompatibleExtensionByIntegerVersionDependency);
395 }
396 }
397
398 /**
399 * @param string $extensionKey
400 * @return bool
401 */
402 protected function isDependentExtensionLoaded($extensionKey) {
403 return ExtensionManagementUtility::isLoaded($extensionKey);
404 }
405
406 /**
407 * @param Dependency $dependency
408 * @return bool
409 */
410 protected function isLoadedVersionCompatible(Dependency $dependency) {
411 $extensionVersion = ExtensionManagementUtility::getExtensionVersion($dependency->getIdentifier());
412 return $this->isVersionCompatible($extensionVersion, $dependency);
413 }
414
415 /**
416 * @param string $version
417 * @param Dependency $dependency
418 * @return bool
419 */
420 protected function isVersionCompatible($version, Dependency $dependency) {
421 if (!($dependency->getLowestVersion() === '') && version_compare($version, $dependency->getLowestVersion()) === -1) {
422 return FALSE;
423 }
424 if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), $version) === -1) {
425 return FALSE;
426 }
427 return TRUE;
428 }
429
430 /**
431 * Checks whether the needed extension is available
432 * (not necessarily installed, but present in system)
433 *
434 * @param string $extensionKey
435 * @return bool
436 */
437 protected function isDependentExtensionAvailable($extensionKey) {
438 $this->setAvailableExtensions();
439 return array_key_exists($extensionKey, $this->availableExtensions);
440 }
441
442 /**
443 * Checks whether the available version is compatible
444 *
445 * @param Dependency $dependency
446 * @return bool
447 */
448 protected function isAvailableVersionCompatible(Dependency $dependency) {
449 $this->setAvailableExtensions();
450 $extensionData = $this->emConfUtility->includeEmConf($this->availableExtensions[$dependency->getIdentifier()]);
451 return $this->isVersionCompatible($extensionData['version'], $dependency);
452 }
453
454 /**
455 * Checks whether a ter extension with $extensionKey exists
456 *
457 * @param string $extensionKey
458 * @return bool
459 */
460 protected function isExtensionDownloadableFromTer($extensionKey) {
461 return $this->extensionRepository->countByExtensionKey($extensionKey) > 0;
462 }
463
464 /**
465 * Checks whether a compatible version of the extension exists in TER
466 *
467 * @param Dependency $dependency
468 * @return bool
469 */
470 protected function isDownloadableVersionCompatible(Dependency $dependency) {
471 $versions = $this->getLowestAndHighestIntegerVersions($dependency);
472 $count = $this->extensionRepository->countByVersionRangeAndExtensionKey(
473 $dependency->getIdentifier(), $versions['lowestIntegerVersion'], $versions['highestIntegerVersion']
474 );
475 return !empty($count);
476 }
477
478 /**
479 * Get the latest compatible version of an extension that
480 * fulfills the given dependency from TER
481 *
482 * @param Dependency $dependency
483 * @return Extension
484 */
485 protected function getLatestCompatibleExtensionByIntegerVersionDependency(Dependency $dependency) {
486 $versions = $this->getLowestAndHighestIntegerVersions($dependency);
487 $compatibleDataSets = $this->extensionRepository->findByVersionRangeAndExtensionKeyOrderedByVersion(
488 $dependency->getIdentifier(),
489 $versions['lowestIntegerVersion'],
490 $versions['highestIntegerVersion']
491 );
492 return $compatibleDataSets->getFirst();
493 }
494
495 /**
496 * Return array of lowest and highest version of dependency as integer
497 *
498 * @param Dependency $dependency
499 * @return array
500 */
501 protected function getLowestAndHighestIntegerVersions(Dependency $dependency) {
502 $lowestVersion = $dependency->getLowestVersion();
503 $lowestVersionInteger = $lowestVersion ? VersionNumberUtility::convertVersionNumberToInteger($lowestVersion) : 0;
504 $highestVersion = $dependency->getHighestVersion();
505 $highestVersionInteger = $highestVersion ? VersionNumberUtility::convertVersionNumberToInteger($highestVersion) : 0;
506 return array(
507 'lowestIntegerVersion' => $lowestVersionInteger,
508 'highestIntegerVersion' => $highestVersionInteger
509 );
510 }
511
512 public function findInstalledExtensionsThatDependOnMe($extensionKey) {
513 $availableAndInstalledExtensions = $this->listUtility->getAvailableAndInstalledExtensionsWithAdditionalInformation();
514 $dependentExtensions = array();
515 foreach ($availableAndInstalledExtensions as $availableAndInstalledExtensionKey => $availableAndInstalledExtension) {
516 if (isset($availableAndInstalledExtension['installed']) && $availableAndInstalledExtension['installed'] === TRUE) {
517 if (is_array($availableAndInstalledExtension['constraints']) && is_array($availableAndInstalledExtension['constraints']['depends']) && array_key_exists($extensionKey, $availableAndInstalledExtension['constraints']['depends'])) {
518 $dependentExtensions[] = $availableAndInstalledExtensionKey;
519 }
520 }
521 }
522 return $dependentExtensions;
523 }
524
525 }