[BUGFIX] Only valid package names will be processed.
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Package / PackageManager.php
1 <?php
2 namespace TYPO3\CMS\Core\Package;
3
4 /* *
5 * This script belongs to the TYPO3 Flow framework. *
6 * *
7 * It is free software; you can redistribute it and/or modify it under *
8 * the terms of the GNU Lesser General Public License, either version 3 *
9 * of the License, or (at your option) any later version. *
10 * *
11 * The TYPO3 project - inspiring people to share! *
12 * */
13
14 use TYPO3\Flow\Annotations as Flow;
15
16 /**
17 * The default TYPO3 Package Manager
18 * Adapted from FLOW for TYPO3 CMS
19 *
20 * @api
21 * @Flow\Scope("singleton")
22 */
23 class PackageManager extends \TYPO3\Flow\Package\PackageManager implements \TYPO3\CMS\Core\SingletonInterface {
24
25 /**
26 * @var \TYPO3\CMS\Core\Core\ClassLoader
27 */
28 protected $classLoader;
29
30 /**
31 * @var \TYPO3\CMS\Core\Package\DependencyResolver
32 */
33 protected $dependencyResolver;
34
35 /**
36 * @var \TYPO3\CMS\Core\Core\Bootstrap
37 */
38 protected $bootstrap;
39
40 /**
41 * @var \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend
42 */
43 protected $coreCache;
44
45 /**
46 * @var string
47 */
48 protected $cacheIdentifier;
49
50 /**
51 * @var array
52 */
53 protected $extAutoloadClassFiles;
54
55 /**
56 * @var array
57 */
58 protected $packagesBasePaths = array();
59
60 /**
61 * @var array
62 */
63 protected $packageAliasMap = array();
64
65 /**
66 * @var array
67 */
68 protected $runtimeActivatedPackages = array();
69
70 /**
71 * Absolute path leading to the various package directories
72 * @var string
73 */
74 protected $packagesBasePath = PATH_site;
75
76 /**
77 * Constructor
78 */
79 public function __construct() {
80 $this->packagesBasePaths = array(
81 'local' => PATH_typo3conf . 'ext',
82 'global' => PATH_typo3 . 'ext',
83 'sysext' => PATH_typo3 . 'sysext',
84 'composer' => PATH_site . 'Packages',
85 );
86 }
87
88 /**
89 * @param \TYPO3\CMS\Core\Core\ClassLoader $classLoader
90 */
91 public function injectClassLoader(\TYPO3\CMS\Core\Core\ClassLoader $classLoader) {
92 $this->classLoader = $classLoader;
93 }
94
95 /**
96 * @param \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache
97 */
98 public function injectCoreCache(\TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache) {
99 $this->coreCache = $coreCache;
100 }
101
102 /**
103 * @param DependencyResolver
104 */
105 public function injectDependencyResolver(DependencyResolver $dependencyResolver) {
106 $this->dependencyResolver = $dependencyResolver;
107 }
108
109 /**
110 * Initializes the package manager
111 *
112 * @param \TYPO3\CMS\Core\Core\Bootstrap|\TYPO3\Flow\Core\Bootstrap $bootstrap The current bootstrap; Flow Bootstrap is here by intention to keep the PackageManager valid to the interface
113 * @return void
114 */
115 public function initialize(\TYPO3\Flow\Core\Bootstrap $bootstrap) {
116 $this->bootstrap = $bootstrap;
117 $this->packageStatesPathAndFilename = PATH_typo3conf . 'PackageStates.php';
118 $this->packageFactory = new PackageFactory($this);
119
120 $this->loadPackageStates();
121
122 $requiredList = array();
123 foreach ($this->packages as $packageKey => $package) {
124 /** @var $package Package */
125 $protected = $package->isProtected();
126 if ($protected) {
127 $requiredList[$packageKey] = $package;
128 }
129 if (isset($this->packageStatesConfiguration['packages'][$packageKey]['state']) && $this->packageStatesConfiguration['packages'][$packageKey]['state'] === 'active') {
130 $this->activePackages[$packageKey] = $package;
131 }
132 }
133 $previousActivePackage = $this->activePackages;
134 $this->activePackages = array_merge($requiredList, $this->activePackages);
135
136 if ($this->activePackages != $previousActivePackage) {
137 foreach ($requiredList as $requiredPackageKey => $package) {
138 $this->packageStatesConfiguration['packages'][$requiredPackageKey]['state'] = 'active';
139 }
140 $this->sortAndSavePackageStates();
141 }
142
143 //@deprecated since 6.2, don't use
144 if (!defined('REQUIRED_EXTENSIONS')) {
145 // List of extensions required to run the core
146 define('REQUIRED_EXTENSIONS', implode(',', array_keys($requiredList)));
147 }
148
149 $cacheIdentifier = $this->getCacheIdentifier();
150 if ($cacheIdentifier === NULL) {
151 // Create an artificial cache identifier if the package states file is not available yet
152 // in order that the class loader and class alias map can cache anyways.
153 $cacheIdentifier = md5(implode('###', array_keys($this->activePackages)));
154 }
155 $this->classLoader->setCacheIdentifier($cacheIdentifier)->setPackages($this->activePackages);
156
157 foreach ($this->activePackages as $package) {
158 /** @var $package Package */
159 $package->boot($bootstrap);
160 }
161
162 $this->saveToPackageCache();
163 }
164
165 /**
166 * @return string
167 */
168 protected function getCacheIdentifier() {
169 if ($this->cacheIdentifier === NULL) {
170 if (@file_exists($this->packageStatesPathAndFilename)) {
171 $this->cacheIdentifier = md5_file($this->packageStatesPathAndFilename);
172 } else {
173 $this->cacheIdentifier = NULL;
174 }
175 }
176 return $this->cacheIdentifier;
177 }
178
179 /**
180 * @return string
181 */
182 protected function getCacheEntryIdentifier() {
183 $cacheIdentifier = $this->getCacheIdentifier();
184 return $cacheIdentifier !== NULL ? 'PackageManager_' . $cacheIdentifier : NULL;
185 }
186
187 /**
188 *
189 */
190 protected function saveToPackageCache() {
191 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
192 if ($cacheEntryIdentifier !== NULL && !$this->coreCache->has($cacheEntryIdentifier)) {
193 // Package objects get their own cache entry, so PHP does not have to parse the serialized string
194 $packageObjectsCacheEntryIdentifier = uniqid('PackageObjects_');
195 // Build cache file
196 $packageCache = array(
197 'packageStatesConfiguration' => $this->packageStatesConfiguration,
198 'packageAliasMap' => $this->packageAliasMap,
199 'packageKeys' => $this->packageKeys,
200 'declaringPackageClassPathsAndFilenames' => array(),
201 'packageObjectsCacheEntryIdentifier' => $packageObjectsCacheEntryIdentifier
202 );
203 foreach ($this->packages as $package) {
204 if (!isset($packageCache['declaringPackageClassPathsAndFilenames'][$packageClassName = get_class($package)])) {
205 $reflectionPackageClass = new \ReflectionClass($packageClassName);
206 $packageCache['declaringPackageClassPathsAndFilenames'][$packageClassName] = $reflectionPackageClass->getFileName();
207 }
208 }
209 $this->coreCache->set($packageObjectsCacheEntryIdentifier, serialize($this->packages));
210 $this->coreCache->set(
211 $cacheEntryIdentifier,
212 'return ' . PHP_EOL .
213 var_export($packageCache, TRUE) . ';'
214 );
215 }
216 }
217
218 /**
219 * Loads the states of available packages from the PackageStates.php file.
220 * The result is stored in $this->packageStatesConfiguration.
221 *
222 * @throws Exception\PackageStatesUnavailableException
223 * @return void
224 */
225 protected function loadPackageStates() {
226 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
227 if ($cacheEntryIdentifier !== NULL && $this->coreCache->has($cacheEntryIdentifier) && $packageCache = $this->coreCache->requireOnce($cacheEntryIdentifier)) {
228 foreach ($packageCache['declaringPackageClassPathsAndFilenames'] as $packageClassPathAndFilename) {
229 require_once $packageClassPathAndFilename;
230 }
231 $this->packageStatesConfiguration = $packageCache['packageStatesConfiguration'];
232 $this->packageAliasMap = $packageCache['packageAliasMap'];
233 $this->packageKeys = $packageCache['packageKeys'];
234 $GLOBALS['TYPO3_currentPackageManager'] = $this;
235 // Strip off PHP Tags from Php Cache Frontend
236 $packageObjects = substr(substr($this->coreCache->get($packageCache['packageObjectsCacheEntryIdentifier']), 6), 0, -2);
237 $this->packages = unserialize($packageObjects);
238 unset($GLOBALS['TYPO3_currentPackageManager']);
239 } else {
240 $this->packageStatesConfiguration = @include($this->packageStatesPathAndFilename) ?: array();
241 if (!isset($this->packageStatesConfiguration['version']) || $this->packageStatesConfiguration['version'] < 4) {
242 $this->packageStatesConfiguration = array();
243 }
244 if ($this->packageStatesConfiguration !== array()) {
245 $this->registerPackagesFromConfiguration();
246 } else {
247 throw new Exception\PackageStatesUnavailableException('The PackageStates.php file is either corrupt or unavailable.', 1381507733);
248 }
249 }
250 }
251
252
253 /**
254 * Scans all directories in the packages directories for available packages.
255 * For each package a Package object is created and stored in $this->packages.
256 *
257 * @return void
258 * @throws \TYPO3\Flow\Package\Exception\DuplicatePackageException
259 */
260 public function scanAvailablePackages() {
261 $previousPackageStatesConfiguration = $this->packageStatesConfiguration;
262
263 if (isset($this->packageStatesConfiguration['packages'])) {
264 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $configuration) {
265 if (!@file_exists($this->packagesBasePath . $configuration['packagePath'])) {
266 unset($this->packageStatesConfiguration['packages'][$packageKey]);
267 }
268 }
269 } else {
270 $this->packageStatesConfiguration['packages'] = array();
271 }
272
273 foreach ($this->packagesBasePaths as $key => $packagesBasePath) {
274 if (!is_dir($packagesBasePath)) {
275 unset($this->packagesBasePaths[$key]);
276 }
277 }
278
279 $packagePaths = $this->scanLegacyExtensions();
280 foreach ($this->packagesBasePaths as $packagesBasePath) {
281 $this->scanPackagesInPath($packagesBasePath, $packagePaths);
282 }
283
284 foreach ($packagePaths as $packagePath => $composerManifestPath) {
285 $packagesBasePath = PATH_site;
286 foreach ($this->packagesBasePaths as $basePath) {
287 if (strpos($packagePath, $basePath) === 0) {
288 $packagesBasePath = $basePath;
289 break;
290 }
291 }
292 try {
293 $composerManifest = self::getComposerManifest($composerManifestPath);
294 $packageKey = PackageFactory::getPackageKeyFromManifest($composerManifest, $packagePath, $packagesBasePath);
295 $this->composerNameToPackageKeyMap[strtolower($composerManifest->name)] = $packageKey;
296 $this->packageStatesConfiguration['packages'][$packageKey]['manifestPath'] = substr($composerManifestPath, strlen($packagePath)) ? : '';
297 $this->packageStatesConfiguration['packages'][$packageKey]['composerName'] = $composerManifest->name;
298 } catch (\TYPO3\Flow\Package\Exception\MissingPackageManifestException $exception) {
299 $relativePackagePath = substr($packagePath, strlen($packagesBasePath));
300 $packageKey = substr($relativePackagePath, strpos($relativePackagePath, '/') + 1, -1);
301 if (!$this->isPackageKeyValid($packageKey)) {
302 continue;
303 }
304 } catch (\TYPO3\Flow\Package\Exception\InvalidPackageKeyException $exception) {
305 continue;
306 }
307 if (!isset($this->packageStatesConfiguration['packages'][$packageKey]['state'])) {
308 $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'inactive';
309 }
310
311 $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $packagePath);
312
313 // Change this to read the target from Composer or any other source
314 $this->packageStatesConfiguration['packages'][$packageKey]['classesPath'] = Package::DIRECTORY_CLASSES;
315 }
316
317 $registerOnlyNewPackages = !empty($this->packages);
318 $this->registerPackagesFromConfiguration($registerOnlyNewPackages);
319 if ($this->packageStatesConfiguration != $previousPackageStatesConfiguration) {
320 $this->sortAndsavePackageStates();
321 }
322 }
323
324 /**
325 * @param array $collectedExtensionPaths
326 * @return array
327 */
328 protected function scanLegacyExtensions(&$collectedExtensionPaths = array()) {
329 $legacyCmsPackageBasePathTypes = array('sysext', 'global', 'local');
330 foreach ($this->packagesBasePaths as $type => $packageBasePath) {
331 if (!in_array($type, $legacyCmsPackageBasePathTypes)) {
332 continue;
333 }
334 /** @var $fileInfo \SplFileInfo */
335 foreach (new \DirectoryIterator($packageBasePath) as $fileInfo) {
336 if (!$fileInfo->isDir()) {
337 continue;
338 }
339 $filename = $fileInfo->getFilename();
340 if ($filename[0] !== '.') {
341 $currentPath = \TYPO3\Flow\Utility\Files::getUnixStylePath($fileInfo->getPathName()) . '/';
342 if (file_exists($currentPath . 'ext_emconf.php')) {
343 $collectedExtensionPaths[$currentPath] = $currentPath;
344 }
345 }
346 }
347 }
348 return $collectedExtensionPaths;
349 }
350
351 /**
352 * Looks for composer.json in the given path and returns a path or NULL.
353 *
354 * @param string $packagePath
355 * @return array
356 */
357 protected function findComposerManifestPaths($packagePath) {
358 // If an ext_emconf.php file is found, we don't need to look deeper
359 if (file_exists($packagePath . '/ext_emconf.php')) {
360 return array();
361 }
362 return parent::findComposerManifestPaths($packagePath);
363 }
364
365 /**
366 * Requires and registers all packages which were defined in packageStatesConfiguration
367 *
368 * @param boolean $registerOnlyNewPackages
369 * @return void
370 * @throws \TYPO3\Flow\Package\Exception\CorruptPackageException
371 */
372 protected function registerPackagesFromConfiguration($registerOnlyNewPackages = FALSE) {
373 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $stateConfiguration) {
374
375 if ($registerOnlyNewPackages && $this->isPackageAvailable($packageKey)) {
376 continue;
377 }
378
379 $packagePath = isset($stateConfiguration['packagePath']) ? $stateConfiguration['packagePath'] : NULL;
380 $classesPath = isset($stateConfiguration['classesPath']) ? $stateConfiguration['classesPath'] : NULL;
381 $manifestPath = isset($stateConfiguration['manifestPath']) ? $stateConfiguration['manifestPath'] : NULL;
382
383 try {
384 $package = $this->packageFactory->create($this->packagesBasePath, $packagePath, $packageKey, $classesPath, $manifestPath);
385 } catch (\TYPO3\Flow\Package\Exception\InvalidPackagePathException $exception) {
386 $this->unregisterPackageByPackageKey($packageKey);
387 continue;
388 } catch (\TYPO3\Flow\Package\Exception\InvalidPackageKeyException $exception) {
389 $this->unregisterPackageByPackageKey($packageKey);
390 continue;
391 }
392
393 $this->registerPackage($package, FALSE);
394
395 if (!$this->packages[$packageKey] instanceof \TYPO3\Flow\Package\PackageInterface) {
396 throw new \TYPO3\Flow\Package\Exception\CorruptPackageException(sprintf('The package class in package "%s" does not implement PackageInterface.', $packageKey), 1300782488);
397 }
398
399 $this->packageKeys[strtolower($packageKey)] = $packageKey;
400 if ($stateConfiguration['state'] === 'active') {
401 $this->activePackages[$packageKey] = $this->packages[$packageKey];
402 }
403 }
404 }
405
406 /**
407 * Register a native Flow package
408 *
409 * @param \TYPO3\Flow\Package\PackageInterface $package The Package to be registered
410 * @param boolean $sortAndSave allows for not saving packagestates when used in loops etc.
411 * @return \TYPO3\Flow\Package\PackageInterface
412 * @throws \TYPO3\Flow\Package\Exception\CorruptPackageException
413 */
414 public function registerPackage(\TYPO3\Flow\Package\PackageInterface $package, $sortAndSave = TRUE) {
415 $package = parent::registerPackage($package, $sortAndSave);
416 if ($package instanceof PackageInterface) {
417 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
418 $this->packageAliasMap[strtolower($packageToReplace)] = $package->getPackageKey();
419 }
420 }
421 return $package;
422 }
423
424 /**
425 * Unregisters a package from the list of available packages
426 *
427 * @param string $packageKey Package Key of the package to be unregistered
428 * @return void
429 */
430 protected function unregisterPackageByPackageKey($packageKey) {
431 try {
432 $package = $this->getPackage($packageKey);
433 if ($package instanceof PackageInterface) {
434 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
435 unset($this->packageAliasMap[strtolower($packageToReplace)]);
436 }
437 $packageKey = $package->getPackageKey();
438 }
439 } catch (\TYPO3\Flow\Package\Exception\UnknownPackageException $e) {
440 }
441 parent::unregisterPackageByPackageKey($packageKey);
442 }
443
444 /**
445 * Resolves a Flow package key from a composer package name.
446 *
447 * @param string $composerName
448 * @return string
449 * @throws \TYPO3\Flow\Package\Exception\InvalidPackageStateException
450 */
451 public function getPackageKeyFromComposerName($composerName) {
452 if (isset($this->packageAliasMap[$composerName])) {
453 return $this->packageAliasMap[$composerName];
454 }
455 try {
456 return parent::getPackageKeyFromComposerName($composerName);
457 } catch (\TYPO3\Flow\Package\Exception\InvalidPackageStateException $exception) {
458 return $composerName;
459 }
460 }
461
462 /**
463 * @return array
464 */
465 public function getExtAutoloadRegistry() {
466 if (!isset($this->extAutoloadClassFiles)) {
467 $classRegistry = array();
468 foreach ($this->activePackages as $packageKey => $packageData) {
469 try {
470 $extensionAutoloadFile = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($packageKey, 'ext_autoload.php');
471 if (@file_exists($extensionAutoloadFile)) {
472 $classRegistry = array_merge($classRegistry, require $extensionAutoloadFile);
473 }
474 } catch (\BadFunctionCallException $e) {
475 }
476 }
477 $this->extAutoloadClassFiles = $classRegistry;
478 }
479 return $this->extAutoloadClassFiles;
480 }
481
482 /**
483 * Returns a PackageInterface object for the specified package.
484 * A package is available, if the package directory contains valid MetaData information.
485 *
486 * @param string $packageKey
487 * @return \TYPO3\Flow\Package\PackageInterface The requested package object
488 * @throws \TYPO3\Flow\Package\Exception\UnknownPackageException if the specified package is not known
489 * @api
490 */
491 public function getPackage($packageKey) {
492 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
493 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
494 }
495 return parent::getPackage($packageKey);
496 }
497
498 /**
499 * Returns TRUE if a package is available (the package's files exist in the packages directory)
500 * or FALSE if it's not. If a package is available it doesn't mean necessarily that it's active!
501 *
502 * @param string $packageKey The key of the package to check
503 * @return boolean TRUE if the package is available, otherwise FALSE
504 * @api
505 */
506 public function isPackageAvailable($packageKey) {
507 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
508 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
509 }
510 return parent::isPackageAvailable($packageKey);
511 }
512
513 /**
514 * Returns TRUE if a package is activated or FALSE if it's not.
515 *
516 * @param string $packageKey The key of the package to check
517 * @return boolean TRUE if package is active, otherwise FALSE
518 * @api
519 */
520 public function isPackageActive($packageKey) {
521 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
522 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
523 }
524 return isset($this->runtimeActivatedPackages[$packageKey]) || parent::isPackageActive($packageKey);
525 }
526
527 /**
528 * @param string $packageKey
529 */
530 public function deactivatePackage($packageKey) {
531 $package = $this->getPackage($packageKey);
532 parent::deactivatePackage($package->getPackageKey());
533 }
534
535 /**
536 * @param string $packageKey
537 */
538 public function activatePackage($packageKey) {
539 $package = $this->getPackage($packageKey);
540 parent::activatePackage($package->getPackageKey());
541 $this->classLoader->addActivePackage($package);
542 }
543
544 /**
545 * Enables packages during runtime, but no class aliases will be available
546 *
547 * @param string $packageKey
548 * @api
549 */
550 public function activatePackageDuringRuntime($packageKey) {
551 $package = $this->getPackage($packageKey);
552 $this->runtimeActivatedPackages[$package->getPackageKey()] = $package;
553 $this->classLoader->addActivePackage($package);
554 }
555
556
557 /**
558 * @param string $packageKey
559 */
560 public function deletePackage($packageKey) {
561 $package = $this->getPackage($packageKey);
562 parent::deletePackage($package->getPackageKey());
563 }
564
565
566 /**
567 * @param string $packageKey
568 */
569 public function freezePackage($packageKey) {
570 $package = $this->getPackage($packageKey);
571 parent::freezePackage($package->getPackageKey());
572 }
573
574 /**
575 * @param string $packageKey
576 * @return bool
577 */
578 public function isPackageFrozen($packageKey) {
579 $package = $this->getPackage($packageKey);
580 return parent::isPackageFrozen($package->getPackageKey());
581 }
582
583 /**
584 * @param string $packageKey
585 */
586 public function unfreezePackage($packageKey) {
587 $package = $this->getPackage($packageKey);
588 parent::unfreezePackage($package->getPackageKey());
589 }
590
591 /**
592 * @param string $packageKey
593 */
594 public function refreezePackage($packageKey) {
595 $package = $this->getPackage($packageKey);
596 parent::refreezePackage($package->getPackageKey());
597 }
598
599 /**
600 * Returns an array of \TYPO3\Flow\Package objects of all active packages.
601 * A package is active, if it is available and has been activated in the package
602 * manager settings. This method returns runtime activated packages too
603 *
604 * @return array Array of \TYPO3\Flow\Package\PackageInterface
605 * @api
606 */
607 public function getActivePackages() {
608 return array_merge(parent::getActivePackages(), $this->runtimeActivatedPackages);
609 }
610
611 /**
612 * Orders all packages by comparing their dependencies. By this, the packages
613 * and package configurations arrays holds all packages in the correct
614 * initialization order.
615 *
616 * @return void
617 */
618 protected function sortAvailablePackagesByDependencies() {
619 $this->resolvePackageDependencies();
620
621 $this->packageStatesConfiguration['packages'] = $this->dependencyResolver->sortPackageStatesConfigurationByDependency($this->packageStatesConfiguration['packages']);
622
623 // Reorder the packages according to the loading order
624 $newPackages = array();
625 foreach (array_keys($this->packageStatesConfiguration['packages']) as $packageKey) {
626 $newPackages[$packageKey] = $this->packages[$packageKey];
627 }
628 $this->packages = $newPackages;
629 }
630
631 /**
632 * Saves the current content of $this->packageStatesConfiguration to the
633 * PackageStates.php file.
634 *
635 * @return void
636 */
637 protected function sortAndSavePackageStates() {
638 parent::sortAndSavePackageStates();
639
640 \TYPO3\CMS\Core\Utility\OpcodeCacheUtility::clearAllActive($this->packageStatesPathAndFilename);
641 }
642
643 /**
644 * Check the conformance of the given package key
645 *
646 * @param string $packageKey The package key to validate
647 * @return boolean If the package key is valid, returns TRUE otherwise FALSE
648 * @api
649 */
650 public function isPackageKeyValid($packageKey) {
651 return parent::isPackageKeyValid($packageKey) || preg_match(\TYPO3\CMS\Core\Package\Package::PATTERN_MATCH_EXTENSIONKEY, $packageKey) === 1;
652 }
653 }