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