InstallUtility.php 22.9 KB
Newer Older
1
<?php
2

3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
7
8
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
9
 *
10
11
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
14
 * The TYPO3 project - inspiring people to share!
 */
15

16
17
namespace TYPO3\CMS\Extensionmanager\Utility;

18
use Psr\EventDispatcher\EventDispatcherInterface;
19
20
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
21
use Symfony\Component\Finder\Finder;
22
use TYPO3\CMS\Core\Cache\CacheManager;
23
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
24
use TYPO3\CMS\Core\Configuration\SiteConfiguration;
25
use TYPO3\CMS\Core\Core\Bootstrap;
26
use TYPO3\CMS\Core\Core\Environment;
27
28
use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
use TYPO3\CMS\Core\Database\Schema\SqlReader;
29
30
use TYPO3\CMS\Core\Package\Event\AfterPackageActivationEvent;
use TYPO3\CMS\Core\Package\Event\AfterPackageDeactivationEvent;
31
32
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Registry;
33
use TYPO3\CMS\Core\Service\OpcodeCacheService;
34
use TYPO3\CMS\Core\SingletonInterface;
35
use TYPO3\CMS\Core\Site\Entity\Site;
36
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
37
use TYPO3\CMS\Core\Utility\GeneralUtility;
38
use TYPO3\CMS\Core\Utility\PathUtility;
39
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
40
use TYPO3\CMS\Extensionmanager\Domain\Model\Extension;
41
42
43
use TYPO3\CMS\Extensionmanager\Event\AfterExtensionDatabaseContentHasBeenImportedEvent;
use TYPO3\CMS\Extensionmanager\Event\AfterExtensionFilesHaveBeenImportedEvent;
use TYPO3\CMS\Extensionmanager\Event\AfterExtensionStaticDatabaseContentHasBeenImportedEvent;
44
use TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException;
45
use TYPO3\CMS\Impexp\Import;
46
use TYPO3\CMS\Impexp\Utility\ImportExportUtility;
47
use TYPO3\CMS\Install\Service\LateBootService;
48

49
50
/**
 * Extension Manager Install Utility
51
 * @internal This class is a specific ExtensionManager implementation and is not part of the Public TYPO3 API.
52
 */
53
class InstallUtility implements SingletonInterface, LoggerAwareInterface
54
{
55
    use LoggerAwareTrait;
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

    /**
     * @var \TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility
     */
    protected $fileHandlingUtility;

    /**
     * @var \TYPO3\CMS\Extensionmanager\Utility\ListUtility
     */
    protected $listUtility;

    /**
     * @var \TYPO3\CMS\Core\Package\PackageManager
     */
    protected $packageManager;

    /**
     * @var \TYPO3\CMS\Core\Cache\CacheManager
     */
    protected $cacheManager;

    /**
78
     * @var \TYPO3\CMS\Core\Registry
79
     */
80
    protected $registry;
81
82

    /**
83
     * @var EventDispatcherInterface
84
     */
85
86
    protected $eventDispatcher;

87
88
89
90
91
    /**
     * @var LateBootService
     */
    protected $lateBootService;

92
93
94
95
    public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher)
    {
        $this->eventDispatcher = $eventDispatcher;
    }
96
97
98
99

    /**
     * @param \TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility $fileHandlingUtility
     */
100
    public function injectFileHandlingUtility(FileHandlingUtility $fileHandlingUtility)
101
102
103
104
105
106
107
    {
        $this->fileHandlingUtility = $fileHandlingUtility;
    }

    /**
     * @param \TYPO3\CMS\Extensionmanager\Utility\ListUtility $listUtility
     */
108
    public function injectListUtility(ListUtility $listUtility)
109
110
111
112
113
114
115
    {
        $this->listUtility = $listUtility;
    }

    /**
     * @param \TYPO3\CMS\Core\Package\PackageManager $packageManager
     */
116
    public function injectPackageManager(PackageManager $packageManager)
117
118
119
120
121
122
123
    {
        $this->packageManager = $packageManager;
    }

    /**
     * @param \TYPO3\CMS\Core\Cache\CacheManager $cacheManager
     */
124
    public function injectCacheManager(CacheManager $cacheManager)
125
126
127
128
129
130
131
    {
        $this->cacheManager = $cacheManager;
    }

    /**
     * @param \TYPO3\CMS\Core\Registry $registry
     */
132
    public function injectRegistry(Registry $registry)
133
134
135
136
    {
        $this->registry = $registry;
    }

137
138
139
140
141
142
143
144
    /**
     * @param  LateBootService $lateBootService
     */
    public function injectLateBootService(LateBootService $lateBootService)
    {
        $this->lateBootService = $lateBootService;
    }

145
146
147
148
    /**
     * Helper function to install an extension
     * also processes db updates and clears the cache if the extension asks for it
     *
149
     * @param string ...$extensionKeys
150
151
     * @throws ExtensionManagerException
     */
152
    public function install(...$extensionKeys)
153
    {
154
155
156
157
158
159
160
161
162
163
164
        $flushCaches = false;
        foreach ($extensionKeys as $extensionKey) {
            $this->loadExtension($extensionKey);
            $extension = $this->enrichExtensionWithDetails($extensionKey, false);
            $this->saveDefaultConfiguration($extensionKey);
            if (!empty($extension['clearcacheonload']) || !empty($extension['clearCacheOnLoad'])) {
                $flushCaches = true;
            }
        }

        if ($flushCaches) {
165
166
167
168
            $this->cacheManager->flushCaches();
        } else {
            $this->cacheManager->flushCachesInGroup('system');
        }
169
170
171
172
173

        // Load a new container as reloadCaches will load ext_localconf
        $container = $this->lateBootService->getContainer();
        $backup = $this->lateBootService->makeCurrent($container);

174
        $this->reloadCaches();
175
        $this->updateDatabase();
176

177
178
        foreach ($extensionKeys as $extensionKey) {
            $this->processExtensionSetup($extensionKey);
179
            $container->get(EventDispatcherInterface::class)->dispatch(new AfterPackageActivationEvent($extensionKey, 'typo3-cms-extension', $this));
180
        }
181
182
183

        // Reset to the original container instance
        $this->lateBootService->makeCurrent(null, $backup);
184
    }
185

186
187
188
    /**
     * @param string $extensionKey
     */
189
    public function processExtensionSetup(string $extensionKey): void
190
    {
191
        $extension = $this->enrichExtensionWithDetails($extensionKey, false);
192
193
194
195
        $this->importInitialFiles($extension['packagePath'], $extensionKey);
        $this->importStaticSqlFile($extensionKey, $extension['packagePath']);
        $import = $this->importT3DFile($extensionKey, $extension['packagePath']);
        $this->importSiteConfiguration($extensionKey, $extension['packagePath'], $import);
196
197
198
199
200
201
202
203
204
205
    }

    /**
     * Helper function to uninstall an extension
     *
     * @param string $extensionKey
     * @throws ExtensionManagerException
     */
    public function uninstall($extensionKey)
    {
206
207
        $dependentExtensions = $this->findInstalledExtensionsThatDependOnExtension((string)$extensionKey);
        if (!empty($dependentExtensions)) {
208
            throw new ExtensionManagerException(
209
                LocalizationUtility::translate(
210
211
                    'extensionList.uninstall.dependencyError',
                    'extensionmanager',
212
                    [$extensionKey, implode(',', $dependentExtensions)]
213
                ) ?? '',
214
215
216
                1342554622
            );
        }
217
        $this->unloadExtension($extensionKey);
218
219
220
    }

    /**
221
222
223
     * Find installed extensions which depend on the given extension.
     * This is used at extension uninstall to stop the process if an installed
     * extension depends on the extension to be uninstalled.
224
225
     *
     * @param string $extensionKey
226
     * @return array
227
     */
228
    protected function findInstalledExtensionsThatDependOnExtension(string $extensionKey): array
229
    {
230
231
232
233
234
235
236
237
238
239
        $availableAndInstalledExtensions = $this->listUtility->getAvailableAndInstalledExtensionsWithAdditionalInformation();
        $dependentExtensions = [];
        foreach ($availableAndInstalledExtensions as $availableAndInstalledExtensionKey => $availableAndInstalledExtension) {
            if (isset($availableAndInstalledExtension['installed']) && $availableAndInstalledExtension['installed'] === true) {
                if (is_array($availableAndInstalledExtension['constraints']) && is_array($availableAndInstalledExtension['constraints']['depends']) && array_key_exists($extensionKey, $availableAndInstalledExtension['constraints']['depends'])) {
                    $dependentExtensions[] = $availableAndInstalledExtensionKey;
                }
            }
        }
        return $dependentExtensions;
240
241
    }

242
243
244
    /**
     * Reset and reload the available extensions
     */
Claus Due's avatar
Claus Due committed
245
246
    public function reloadAvailableExtensions()
    {
247
248
249
        $this->listUtility->reloadAvailableExtensions();
    }

250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
    /**
     * Wrapper function for loading extensions
     *
     * @param string $extensionKey
     */
    protected function loadExtension($extensionKey)
    {
        $this->packageManager->activatePackage($extensionKey);
    }

    /**
     * Wrapper function for unloading extensions
     *
     * @param string $extensionKey
     */
    protected function unloadExtension($extensionKey)
    {
        $this->packageManager->deactivatePackage($extensionKey);
268
        $this->eventDispatcher->dispatch(new AfterPackageDeactivationEvent($extensionKey, 'typo3-cms-extension', $this));
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
        $this->cacheManager->flushCachesInGroup('system');
    }

    /**
     * Checks if an extension is available in the system
     *
     * @param string $extensionKey
     * @return bool
     */
    public function isAvailable($extensionKey)
    {
        return $this->packageManager->isPackageAvailable($extensionKey);
    }

    /**
     * Reloads the package information, if the package is already registered
     *
     * @param string $extensionKey
     * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageStateException if the package isn't available
     * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageKeyException if an invalid package key was passed
     * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackagePathException if an invalid package path was passed
     * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageManifestException if no extension configuration file could be found
     */
    public function reloadPackageInformation($extensionKey)
    {
        if ($this->packageManager->isPackageAvailable($extensionKey)) {
            $this->reloadOpcache();
            $this->packageManager->reloadPackageInformation($extensionKey);
        }
    }

    /**
     * Fetch additional information for an extension key
     *
     * @param string $extensionKey
304
     * @param bool $loadTerInformation
305
306
     * @return array
     * @throws ExtensionManagerException
307
     * @internal
308
     */
309
    public function enrichExtensionWithDetails($extensionKey, $loadTerInformation = true)
310
    {
311
        $extension = $this->getExtensionArray($extensionKey);
312
        if (!$loadTerInformation) {
313
314
315
            $availableAndInstalledExtensions = $this->listUtility->enrichExtensionsWithEmConfInformation([$extensionKey => $extension]);
        } else {
            $availableAndInstalledExtensions = $this->listUtility->enrichExtensionsWithEmConfAndTerInformation([$extensionKey => $extension]);
316
317
        }

318
319
320
321
322
323
324
325
326
327
        if (!isset($availableAndInstalledExtensions[$extensionKey])) {
            throw new ExtensionManagerException(
                'Please check your uploaded extension "' . $extensionKey . '". The configuration file "ext_emconf.php" seems to be invalid.',
                1391432222
            );
        }

        return $availableAndInstalledExtensions[$extensionKey];
    }

328
329
330
331
332
333
334
335
336
337
338
    /**
     * @param string $extensionKey
     * @return array
     * @throws ExtensionManagerException
     */
    protected function getExtensionArray($extensionKey)
    {
        $availableExtensions = $this->listUtility->getAvailableExtensions();
        if (isset($availableExtensions[$extensionKey])) {
            return $availableExtensions[$extensionKey];
        }
339
        throw new ExtensionManagerException('Extension ' . $extensionKey . ' is not available', 1342864081);
340
341
    }

342
343
344
345
346
347
    /**
     * Reload Cache files and Typo3LoadedExtensions
     */
    public function reloadCaches()
    {
        $this->reloadOpcache();
348
349
350
        ExtensionManagementUtility::loadExtLocalconf(false);
        Bootstrap::loadBaseTca(false);
        Bootstrap::loadExtTables(false);
351
352
353
354
355
356
357
358
359
360
    }

    /**
     * Reloads PHP opcache
     */
    protected function reloadOpcache()
    {
        GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
    }

361
362
363
364
    /**
     * Executes all safe database statements.
     * Tables and fields are created and altered. Nothing gets deleted or renamed here.
     */
365
    protected function updateDatabase()
366
367
368
369
370
371
372
373
374
    {
        $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
        $schemaMigrator = GeneralUtility::makeInstance(SchemaMigrator::class);
        $sqlStatements = [];
        $sqlStatements[] = $sqlReader->getTablesDefinitionString();
        $sqlStatements = $sqlReader->getCreateTableStatementArray(implode(LF . LF, array_filter($sqlStatements)));
        $updateStatements = $schemaMigrator->getUpdateSuggestions($sqlStatements);

        $updateStatements = array_merge_recursive(...array_values($updateStatements));
375
        $selectedStatements = [];
376
377
378
379
        foreach (['add', 'change', 'create_table', 'change_table'] as $action) {
            if (empty($updateStatements[$action])) {
                continue;
            }
380
381
382

            $statements = array_combine(array_keys($updateStatements[$action]), array_fill(0, count($updateStatements[$action]), true));
            $statements = is_array($statements) ? $statements : [];
383
384
            $selectedStatements = array_merge(
                $selectedStatements,
385
                $statements
386
            );
387
388
        }

389
        $schemaMigrator->migrate($sqlStatements, $selectedStatements);
390
391
    }

392
393
394
395
396
397
398
    /**
     * Save default configuration of an extension
     *
     * @param string $extensionKey
     */
    protected function saveDefaultConfiguration($extensionKey)
    {
399
        $extensionConfiguration = GeneralUtility::makeInstance(ExtensionConfiguration::class);
400
        $extensionConfiguration->synchronizeExtConfTemplateWithLocalConfiguration($extensionKey);
401
402
403
404
405
406
407
408
409
    }

    /**
     * Import static SQL data (normally used for ext_tables_static+adt.sql)
     *
     * @param string $rawDefinitions
     */
    public function importStaticSql($rawDefinitions)
    {
410
411
412
413
414
        $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
        $statements = $sqlReader->getStatementArray($rawDefinitions);

        $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
        $schemaMigrationService->importStaticData($statements, true);
415
416
417
418
419
420
421
422
423
424
    }

    /**
     * Remove an extension (delete the directory)
     *
     * @param string $extension
     * @throws ExtensionManagerException
     */
    public function removeExtension($extension)
    {
425
        $absolutePath = $this->enrichExtensionWithDetails($extension)['packagePath'];
426
        if ($this->isValidExtensionPath($absolutePath)) {
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
            if ($this->packageManager->isPackageAvailable($extension)) {
                // Package manager deletes the extension and removes the entry from PackageStates.php
                $this->packageManager->deletePackage($extension);
            } else {
                // The extension is not listed in PackageStates.php, we can safely remove it
                $this->fileHandlingUtility->removeDirectory($absolutePath);
            }
        } else {
            throw new ExtensionManagerException('No valid extension path given.', 1342875724);
        }
    }

    /**
     * Uses the export import extension to import a T3D or XML file to PID 0
     * Execution state is saved in the this->registry, so it only happens once
     *
443
     * @param string $extensionKey
444
     * @param string $packagePath
445
     * @return Import|null
446
     */
447
    protected function importT3DFile($extensionKey, $packagePath): ?Import
448
    {
449
        $extensionSiteRelPath = PathUtility::stripPathSitePrefix($packagePath);
450
        $registryKeysToCheck = [
451
452
            $extensionSiteRelPath . 'Initialisation/data.t3d',
            $extensionSiteRelPath . 'Initialisation/dataImported',
453
        ];
454
455
456
        foreach ($registryKeysToCheck as $registryKeyToCheck) {
            if ($this->registry->get('extensionDataImport', $registryKeyToCheck)) {
                // Data was imported before => early return
457
                return null;
458
459
460
            }
        }
        $importFileToUse = null;
461
        $possibleImportFiles = [
462
463
            $packagePath . 'Initialisation/data.t3d',
            $packagePath . 'Initialisation/data.xml'
464
        ];
465
        foreach ($possibleImportFiles as $possibleImportFile) {
466
            if (!file_exists($possibleImportFile)) {
467
468
469
470
471
                continue;
            }
            $importFileToUse = $possibleImportFile;
        }
        if ($importFileToUse !== null) {
472
            $importExportUtility = GeneralUtility::makeInstance(ImportExportUtility::class);
473
            try {
474
                $importResult = $importExportUtility->importT3DFile($importFileToUse, 0);
475
                $this->registry->set('extensionDataImport', $extensionSiteRelPath . 'Initialisation/dataImported', 1);
476
                $this->eventDispatcher->dispatch(new AfterExtensionDatabaseContentHasBeenImportedEvent($extensionKey, $importFileToUse, $importResult, $this));
477
                return $importExportUtility->getImport();
478
            } catch (\ErrorException $e) {
479
                $this->logger->warning($e->getMessage(), ['exception' => $e]);
480
481
            }
        }
482
        return null;
483
484
485
486
487
488
    }

    /**
     * Imports a static tables SQL File (ext_tables_static+adt)
     * Execution state is saved in the this->registry, so it only happens once
     *
489
     * @param string $extensionKey
490
     * @param string $packagePath
491
     */
492
    protected function importStaticSqlFile(string $extensionKey, $packagePath)
493
    {
494
495
        $extTablesStaticSqlFile = $packagePath . 'ext_tables_static+adt.sql';
        $extTablesStaticSqlRelFile = PathUtility::stripPathSitePrefix($extTablesStaticSqlFile);
496
        if (!$this->registry->get('extensionDataImport', $extTablesStaticSqlRelFile)) {
497
            $shortFileHash = '';
498
            if (file_exists($extTablesStaticSqlFile)) {
499
                $extTablesStaticSqlContent = (string)file_get_contents($extTablesStaticSqlFile);
500
                $shortFileHash = md5($extTablesStaticSqlContent);
501
502
                $this->importStaticSql($extTablesStaticSqlContent);
            }
503
            $this->registry->set('extensionDataImport', $extTablesStaticSqlRelFile, $shortFileHash);
504
            $this->eventDispatcher->dispatch(new AfterExtensionStaticDatabaseContentHasBeenImportedEvent($extensionKey, $extTablesStaticSqlFile, $this));
505
506
507
508
509
510
511
        }
    }

    /**
     * Imports files from Initialisation/Files to fileadmin
     * via lowlevel copy directory method
     *
512
     * @param string $packagePath absolute path to extension dir
513
514
     * @param string $extensionKey
     */
515
    protected function importInitialFiles($packagePath, $extensionKey)
516
    {
517
518
        $importFolder = $packagePath . 'Initialisation/Files';
        $importRelFolder = PathUtility::stripPathSitePrefix($importFolder);
519
520
        if (!$this->registry->get('extensionDataImport', $importRelFolder)) {
            if (file_exists($importFolder)) {
521
                $destinationAbsolutePath = GeneralUtility::getFileAbsFileName($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] . $extensionKey);
522
523
524
525
526
                if (!file_exists($destinationAbsolutePath) &&
                    GeneralUtility::isAllowedAbsPath($destinationAbsolutePath)
                ) {
                    GeneralUtility::mkdir($destinationAbsolutePath);
                }
527
                GeneralUtility::copyDirectory($importFolder, $destinationAbsolutePath);
528
                $this->registry->set('extensionDataImport', $importRelFolder, 1);
529
                $this->eventDispatcher->dispatch(new AfterExtensionFilesHaveBeenImportedEvent($extensionKey, $destinationAbsolutePath, $this));
530
531
532
533
            }
        }
    }

534
    /**
535
536
     * @param string $extensionKey
     * @param string $packagePath
537
     * @param Import|null $import
538
     */
539
    protected function importSiteConfiguration(string $extensionKey, string $packagePath, Import $import = null): void
540
    {
541
        $importAbsFolder = $packagePath . 'Initialisation/Site';
542
543
544
545
546
547
        $destinationFolder = Environment::getConfigPath() . '/sites';

        if (!is_dir($importAbsFolder)) {
            return;
        }

548
        $siteConfiguration = GeneralUtility::makeInstance(SiteConfiguration::class);
549
550
        $existingSites = $siteConfiguration->resolveAllExistingSites(false);

551
552
553
554
555
        GeneralUtility::mkdir($destinationFolder);
        $finder = GeneralUtility::makeInstance(Finder::class);
        $finder->directories()->in($importAbsFolder);
        if ($finder->hasResults()) {
            foreach ($finder as $siteConfigDirectory) {
556
557
                $siteIdentifier = $siteConfigDirectory->getBasename();
                if (isset($existingSites[$siteIdentifier])) {
558
                    $this->logger->warning(
559
560
                        sprintf(
                            'Skipped importing site configuration from %s due to existing site identifier %s',
561
                            $extensionKey,
562
563
564
565
566
567
568
                            $siteIdentifier
                        )
                    );
                    continue;
                }
                $targetDir = $destinationFolder . '/' . $siteIdentifier;
                if (!$this->registry->get('siteConfigImport', $siteIdentifier) && !is_dir($targetDir)) {
569
570
                    GeneralUtility::mkdir($targetDir);
                    GeneralUtility::copyDirectory($siteConfigDirectory->getPathname(), $targetDir);
571
                    $this->registry->set('siteConfigImport', $siteIdentifier, 1);
572
573
574
                }
            }
        }
575
576
577
578
579
580
581
582
583

        /** @var Site[] $newSites */
        $newSites = array_diff_key($siteConfiguration->resolveAllExistingSites(false), $existingSites);
        $importedPages = $import->import_mapId['pages'] ?? null;

        foreach ($newSites as $newSite) {
            $exportedPageId = $newSite->getRootPageId();
            $importedPageId = $importedPages[$exportedPageId] ?? null;
            if ($importedPageId === null) {
584
                $this->logger->warning(
585
586
587
588
589
590
591
592
593
594
595
                    sprintf(
                        'Imported site configuration with identifier %s could not be mapped to imported page id',
                        $newSite->getIdentifier()
                    )
                );
                continue;
            }
            $configuration = $siteConfiguration->load($newSite->getIdentifier());
            $configuration['rootPageId'] = $importedPageId;
            $siteConfiguration->write($newSite->getIdentifier(), $configuration);
        }
596
    }
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613

    /**
     * Is the given path a valid path for extension installation
     *
     * @param string $path the absolute (!) path in question
     * @return bool
     */
    protected function isValidExtensionPath($path): bool
    {
        $allowedPaths = Extension::returnAllowedInstallPaths();
        foreach ($allowedPaths as $allowedPath) {
            if (GeneralUtility::isFirstPartOfStr($path, $allowedPath)) {
                return true;
            }
        }
        return false;
    }
614
}