diff --git a/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/acquia_starterkit_site_studio.info.yml b/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/acquia_starterkit_site_studio.info.yml new file mode 100644 index 000000000..966486db9 --- /dev/null +++ b/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/acquia_starterkit_site_studio.info.yml @@ -0,0 +1,7 @@ +name: 'Acquia Starter Kit Site Studio' +package: 'Acquia Starter Kit' +description: 'Module for Site Studio package import from recipes.' +core_version_requirement: ^10.3 || ^11 +type: module +dependencies: + - acquia_cms_site_studio:acquia_cms_site_studio diff --git a/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/acquia_starterkit_site_studio.services.yml b/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/acquia_starterkit_site_studio.services.yml new file mode 100644 index 000000000..95dc5bce9 --- /dev/null +++ b/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/acquia_starterkit_site_studio.services.yml @@ -0,0 +1,8 @@ +services: + acquia_starterkit_site_studio.package_source.default_recipe_package: + class: Drupal\acquia_starterkit_site_studio\Services\DefaultRecipePackage + arguments: + - '@module_handler' + - '@config.factory' + tags: + - { name: sitestudio_package_source, priority: 50 } diff --git a/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/config/site_studio/site_studio.packages.yml b/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/config/site_studio/site_studio.packages.yml new file mode 100644 index 000000000..002c5f344 --- /dev/null +++ b/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/config/site_studio/site_studio.packages.yml @@ -0,0 +1,164 @@ +- + type: default_module_package + source: + module_name: acquia_cms_site_studio + path: config/pack_acquia_cms_core + options: + extra-validation: false +- + type: default_recipe_package + source: + dependencies: + recipes: + - acquia_starterkit_remote_video + recipe_name: acquia_starterkit_remote_video + path: site_studio/pack_acquia_starterkit_remote_video + options: + extra-validation: false +- + type: default_module_package + source: + dependencies: + - acquia_cms_search + module_name: acquia_cms_search + path: config/pack_acquia_cms_search + options: + extra-validation: false +- + type: default_recipe_package + source: + dependencies: + recipes: + - acquia_starterkit_image + recipe_name: acquia_starterkit_image + path: site_studio/pack_acquia_starterkit_image + options: + extra-validation: false +- + type: default_recipe_package + source: + dependencies: + recipes: + - acquia_starterkit_image + recipe_name: acquia_starterkit_image + path: site_studio/pack_acquia_starterkit_image_core + options: + extra-validation: false +- + type: default_recipe_package + source: + dependencies: + recipes: + - acquia_starterkit_page + recipe_name: acquia_starterkit_page + path: site_studio/pack_acquia_starterkit_page + options: + extra-validation: false +- + type: default_recipe_package + source: + dependencies: + recipes: + - acquia_starterkit_place + recipe_name: acquia_starterkit_place + path: site_studio/pack_acquia_starterkit_place + options: + extra-validation: false +- + type: default_recipe_package + source: + dependencies: + modules: + - acquia_cms_search + recipes: + - acquia_starterkit_place + recipe_name: acquia_starterkit_place + path: site_studio/pack_acquia_starterkit_place_search + options: + extra-validation: false +- + type: default_recipe_package + source: + dependencies: + recipes: + - acquia_starterkit_event + recipe_name: acquia_starterkit_event + path: site_studio/pack_acquia_starterkit_event + options: + extra-validation: false +- + type: default_recipe_package + source: + dependencies: + modules: + - acquia_cms_search + recipes: + - acquia_starterkit_event + recipe_name: acquia_starterkit_event + path: site_studio/pack_acquia_starterkit_event_search + options: + extra-validation: false +- + type: default_recipe_package + source: + dependencies: + recipes: + - acquia_starterkit_person + recipe_name: acquia_starterkit_person + path: site_studio/pack_acquia_starterkit_person + options: + extra-validation: false +- + type: default_recipe_package + source: + dependencies: + modules: + - acquia_cms_search + recipes: + - acquia_starterkit_person + recipe_name: acquia_starterkit_person + path: site_studio/pack_acquia_starterkit_person_search + options: + extra-validation: false +- + type: default_recipe_package + source: + dependencies: + recipes: + - acquia_starterkit_article + recipe_name: acquia_starterkit_article + path: site_studio/pack_acquia_starterkit_article + options: + extra-validation: false +- + type: default_recipe_package + source: + dependencies: + modules: + - acquia_cms_search + recipes: + - acquia_starterkit_article + recipe_name: acquia_starterkit_article + path: site_studio/pack_acquia_starterkit_article_search + options: + extra-validation: false +- + type: default_recipe_package + source: + dependencies: + recipes: + - acquia_starterkit_dam + recipe_name: acquia_starterkit_dam + path: site_studio/pack_acquia_starterkit_dam + options: + extra-validation: false +- + type: default_recipe_package + source: + dependencies: + recipes: + - acquia_starterkit_remote_audio + recipe_name: acquia_starterkit_remote_audio + path: site_studio/pack_acquia_starterkit_remote_audio + options: + extra-validation: false diff --git a/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/src/Plugin/ConfigAction/BasePackageImport.php b/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/src/Plugin/ConfigAction/BasePackageImport.php new file mode 100644 index 000000000..1e326d135 --- /dev/null +++ b/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/src/Plugin/ConfigAction/BasePackageImport.php @@ -0,0 +1,146 @@ +get('config.factory'), + $container->get('settings'), + $container->get('module_handler'), + ); + } + + /** + * {@inheritdoc} + */ + public function apply(string $configName, mixed $value): void { + if ($configName === 'cohesion.settings' && $value) { + $this->updateConfig($configName); + } + } + + /** + * Updates the configuration with API and organization keys. + * + * @param string $configName + * The name of the configuration. + */ + private function updateConfig(string $configName): void { + $config = $this->configFactory->get($configName); + $apiKey = $config->get('api_key'); + $orgKey = $config->get('organization_key'); + + if (!($apiKey && $orgKey)) { + $apiKey = getenv('SITESTUDIO_API_KEY') ?? $this->settings->get('cohesion.settings')->get('api_key'); + $orgKey = getenv('SITESTUDIO_ORG_KEY') ?? $this->settings->get('cohesion.settings')->get('organization_key'); + if (!($apiKey && $orgKey)) { + return; + } + } + + $this->configFactory->getEditable($configName) + ->set('api_key', $apiKey) + ->set('organization_key', $orgKey) + ->save(TRUE); + + if (PHP_SAPI === 'cli' && !function_exists('drush_backend_batch_process')) { + $this->notifyManualImport(); + } else { + $this->importBasePackages(); + } + } + + /** + * Notifies the user to import packages manually from the UI. + */ + private function notifyManualImport(): void { + $output = new SymfonyStyle(new ArgvInput(), new ConsoleOutput()); + $output->warning($this->toPlainString( + t('Please import site studio packages from UI: @uri.', [ + '@uri' => '/admin/cohesion/configuration/account-settings' + ]))); + } + + /** + * Converts a stringable like TranslatableMarkup to a plain text string. + * + * @param \Stringable|string $text + * The string to convert. + * + * @return string + * The plain text string. + */ + private function toPlainString(\Stringable|string $text): string { + return PlainTextOutput::renderFromHtml((string) $text); + } + + /** + * Imports the base Site Studio packages. + */ + private function importBasePackages(): void { + $this->initializeBatch(); + $this->processBatchIfCli(); + } + + /** + * Initializes the batch process for importing packages. + */ + private function initializeBatch(): void { + batch_set(AdministrationController::batchAction(TRUE)); + } + + /** + * Processes the batch if running in CLI mode. + */ + private function processBatchIfCli(): void { + if (PHP_SAPI === 'cli') { + drush_backend_batch_process(); + } + } + +} diff --git a/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/src/Services/DefaultRecipePackage.php b/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/src/Services/DefaultRecipePackage.php new file mode 100644 index 000000000..b438e0800 --- /dev/null +++ b/modules/acquia_cms_site_studio/modules/acquia_starterkit_site_studio/src/Services/DefaultRecipePackage.php @@ -0,0 +1,158 @@ +moduleHandler = $moduleHandler; + $this->configFactory = $configFactory; + } + + /** + * {@inheritdoc} + */ + public function supportedType(string $type): bool { + return $type === self::SUPPORTED_TYPE; + } + + /** + * {@inheritdoc} + */ + public function getSupportedType(): string { + return self::SUPPORTED_TYPE; + } + + /** + * Handles default recipe packages. + * + * No downloading/file moving actions are required, so we're + * checking for correct metadata values and returning sync dir path. + * + * @param array $sourceMetadata + * Source metadata. + * + * @return string + * Path to default package directory in recipe. + * + * @throws \Exception + * Thrown if source metadata values are missing. + */ + public function preparePackage(array $sourceMetadata): string { + $dependencies = $sourceMetadata['dependencies'] ?? []; + $recipe_applied = $this->configFactory->get('acquia_starterkit_core.settings')->get('recipes_applied'); + if ($dependencies) { + if (!empty($dependencies['recipes'])) { + foreach ($dependencies['recipes'] as $dependency) { + if (!$this->recipeExist($dependency) || !in_array($sourceMetadata['recipe_name'], $recipe_applied)) { + return ""; + } + } + } + if (!empty($dependencies['modules'])) { + foreach ($dependencies['modules'] as $dependency) { + if (!$this->moduleHandler->moduleExists($dependency)) { + return ""; + } + } + } + } + $this->validateMetadata($sourceMetadata); + + $recipe_path = $this->getRecipePath($sourceMetadata['recipe_name']); + $package_path = $sourceMetadata['path']; + + return $recipe_path . '/' . $package_path; + } + + /** + * Validates Source metadata. + * + * @param array $sourceMetadata + * Metadata passed to Source service. + * + * @return void + */ + protected function validateMetadata(array $sourceMetadata): void { + $missingProperties = array_diff(self::REQUIRED_PROPERTIES, array_keys($sourceMetadata)); + if (!empty($missingProperties)) { + throw new PackageSourceMissingPropertiesException(self::SUPPORTED_TYPE, $missingProperties, self::REQUIRED_PROPERTIES); + } + + if (!$this->recipeExist($sourceMetadata['recipe_name'])) { + throw new MissingDependencyException(sprintf('Missing recipe: %s', $sourceMetadata['recipe_name'])); + } + + $recipePath = $this->getRecipePath($sourceMetadata['recipe_name']); + $packagePath = $recipePath . '/' . $sourceMetadata['path']; + if (!is_dir($packagePath) || !is_readable($packagePath)) { + throw new DirectoryNotReadyException(sprintf('Directory not found or not readable: %s', $packagePath)); + } + } + + /** + * Checks if a recipe exists. + * + * @param string $recipeName + * The name of the recipe. + * + * @return bool + * TRUE if the recipe exists, FALSE otherwise. + */ + protected function recipeExist(string $recipe_name): bool { + $recipe_path = $this->getRecipePath($recipe_name) . '/recipe.yml'; + return file_exists($recipe_path); + } + + /** + * Gets the path to the recipe. + * + * @param string $recipeName + * The name of the recipe. + * + * @return string + * The path to the recipe. + */ + public function getRecipePath(string $recipe_name): string { + return \Drupal::root() . '/recipes/' . $recipe_name; + } + +}