rocketship_core-8.x-2.0-alpha11/rocketship_core.module
rocketship_core.module
<?php /** * @file * Main module file. */ use Drupal\block\Entity\Block; use Drupal\Component\Utility\Random; use Drupal\Core\Entity\ContentEntityFormInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\TranslatableInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Url; use Drupal\menu_link_content\MenuLinkContentInterface; use Drupal\path_alias\PathAliasInterface; use Drupal\rocketship_core\Event\PathAliasUpdateEvent; use Drupal\user\Entity\Role; use Symfony\Component\Yaml\Yaml; /** * Implements hook_token_info(). */ function rocketship_core_token_info() { // Add a token for the alias of the parent menu link. $info['tokens']['menu-link']['parent-alias'] = [ 'name' => t('Menu parent: alias'), 'description' => t('URL alias of the menu parent.'), ]; $info['tokens']['current-page']['paged-url'] = [ 'name' => t('Paged URL'), 'description' => t('The URL of the current page including the page query parameter.'), 'type' => 'url', ]; return $info; } /** * Implements hook_tokens(). */ function rocketship_core_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { $replacements = []; $language_manager = \Drupal::languageManager(); $url_options = ['absolute' => TRUE]; if (isset($options['langcode'])) { $url_options['language'] = $language_manager->getLanguage($options['langcode']); } if ($type == 'menu-link' && !empty($data['menu-link'])) { $link = $data['menu-link']; $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); if ($link instanceof MenuLinkContentInterface) { $link = $menu_link_manager->createInstance($link->getPluginId()); } foreach ($tokens as $name => $original) { switch ($name) { case 'parent-alias': if ($link->getParent() && $parent = $menu_link_manager->createInstance($link->getParent())) { $alias_manager = \Drupal::service('path_alias.manager'); $url = $parent->getUrlObject(); if (!$url->isExternal()) { $path = '/' . $url->getInternalPath(); $replacements[$original] = $alias_manager->getAliasByPath($path); } } break; } } } // Current page tokens. if ($type == 'current-page') { $request = \Drupal::request(); foreach ($tokens as $name => $original) { switch ($name) { // Returns the current page url + the page query parameter if present. case 'paged-url': $page = $request->query->get('page', NULL); if ($page) { $url_options['query']['page'] = $page; } $bubbleable_metadata->addCacheContexts(['url']); try { $url = Url::createFromRequest($request)->setOptions($url_options); } catch (\Exception $e) { // Url::createFromRequest() can fail, e.g. on 404 pages. // Fall back and try again with Url::fromUserInput(). try { $url = Url::fromUserInput($request->getPathInfo(), $url_options); } catch (\Exception $e) { // Instantiation would fail again on malformed urls. } } if (isset($url)) { $replacements[$original] = $url->toString(); } break; } } } return $replacements; } /** * Implements hook_ENTITY_TYPE_update() for 'path_alias'. */ function rocketship_core_path_alias_update(PathAliasInterface $path_alias) { /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher */ $event_dispatcher = \Drupal::service('event_dispatcher'); // Dispatch the path alias update event. $event = new PathAliasUpdateEvent($path_alias); $event_dispatcher->dispatch(PathAliasUpdateEvent::PATH_ALIAS_UPDATE, $event); } /** * Implements hook_BASE_FORM_ID_alter(). * * If the field_header_paragraph is present, and that node is set as the * frontpage, hide the header paragraph field. */ function rocketship_core_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) { if (isset($form['field_header_paragraph'])) { /** @var \Drupal\Core\Entity\EntityForm $form_object */ $form_object = $form_state->getFormObject(); /** @var \Drupal\node\NodeInterface $node */ $node = $form_object->getEntity(); if ($node->isNew()) { return; } $front = \Drupal::configFactory()->get('system.site')->get('page.front'); if ("/node/{$node->id()}" == $front) { // Hide the paragraph header field // $form['field_header_paragraph']['#access'] = FALSE; // Unset it, #access => FALSE triggers ImageWidget::validateRequiredFields // for some godforsaken reason. unset($form['field_header_paragraph']); } } } /** * Implements hook_form_alter(). * * We're not using the language selector, instead we're just making it clear * to the user in what language they're working in. */ function rocketship_core_form_alter(&$form, FormStateInterface $form_state, $form_id) { // Only on admin pages and for logged in users. if (!\Drupal::service('router.admin_context')->isAdminRoute() || !\Drupal::currentUser()->isAuthenticated()) { return; } $object = $form_state->getFormObject(); if ($object instanceof ContentEntityFormInterface) { $entity = $object->getEntity(); if ($entity) { $language = $entity->language()->getName(); $text = t('Creating @entity_type in @language', [ '@language' => $language, '@entity_type' => $entity->getEntityType()->getLabel(), ]); if (!$entity->isNew()) { $text = t('Editing @language translation', ['@language' => $language]); } if ($entity instanceof TranslatableInterface) { if ($entity->isNewTranslation() && !$entity->isNew()) { $text = t('Adding @language translation', ['@language' => $language]); } } $form['rocketship_core_language_info'] = [ '#weight' => -9999, '#markup' => "<h4>$text</h4>", ]; } } } /** * Implements hook_theme(). */ function rocketship_core_theme() { $return = [ 'idt_widget' => [ 'render element' => 'element', 'file' => 'rocketship_core.field.inc', ], 'label_value_list_item' => [ 'variables' => ['label' => NULL, 'value' => NULL, 'promoted' => FALSE], ], 'title_description_list_item' => [ 'variables' => [ 'title' => NULL, 'description' => NULL, 'wrapper' => NULL, ], ], ]; return $return; } /** * Implements hook_theme_registry_alter(). */ function rocketship_core_theme_registry_alter(&$theme_registry) { $layouts = \Drupal::service('plugin.manager.core.layout')->getDefinitions(); $layout_theme_hooks = []; /** @var \Drupal\Core\Layout\LayoutDefinition $info */ foreach ($layouts as $info) { if ($info->getCategory() == 'Rocketship Layouts - Display Suite') { $layout_theme_hooks[$info->getThemeHook()] = 'layout'; } if ($info->getCategory() == 'Rocketship Layouts - Panels') { $layout_theme_hooks[$info->getThemeHook()] = 'layout'; } } // Only add preprocess functions if entity exposes theme function, and this // layout is using the Display Suite layout class. foreach ($theme_registry as $theme_hook => $info) { if (array_key_exists($theme_hook, $layout_theme_hooks) || (!empty($info['base hook']) && array_key_exists($info['base hook'], $layout_theme_hooks))) { // @todo Remove once https://www.drupal.org/node/2861840 is resolved. if (!in_array('template_preprocess_layout', $theme_registry[$theme_hook]['preprocess functions'])) { $theme_registry[$theme_hook]['preprocess functions'][] = 'template_preprocess_layout'; } } } // ------------------------------------------------------------------------ // Workaround to get theme suggestions working for templates using the // the Display Suite class. It's borderline insane, but gets the job done. // // Note that this currently only works for Twig, but I assume, there isn't // any other engine out there yet for Drupal 8. // // Code based on drupal_find_theme_templates(). // // @see // - https://www.drupal.org/node/2862683 (core queue) // - https://www.drupal.org/node/2802429 (DS queue) // (and maybe others) // ------------------------------------------------------------------------. // Merge layout and field hooks. $all_ds_theme_hooks = $layout_theme_hooks; $engine = \Drupal::theme()->getActiveTheme()->getEngine(); if ($engine == 'twig') { $extension = '.html.twig'; $theme_path = \Drupal::theme()->getActiveTheme()->getPath(); // Escape the periods in the extension. $regex = '/' . str_replace('.', '\.', $extension) . '$/'; // Get a listing of all template files in the path to search. $files = \Drupal::service('file_system') ->scanDirectory($theme_path, $regex, ['key' => 'filename']); $patterns = array_keys($files); $implementations = []; foreach ($all_ds_theme_hooks as $hook => $base_hook) { // Ignored if not registered (which would be weird). if (!isset($theme_registry[$hook])) { continue; } $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__'); if (!empty($pattern)) { // Transform _ in pattern to - to match file naming scheme // for the purposes of searching. $pattern = strtr($pattern, '_', '-'); $matches = preg_grep('/^' . $pattern . '/', $patterns); if ($matches) { foreach ($matches as $match) { $file = $match; // Remove the extension from the filename. $file = str_replace($extension, '', $file); // Put the underscores back in for the hook name and register this // pattern. $info = $theme_registry[$hook]; $arg_name = isset($info['variables']) ? 'variables' : 'render element'; $new_hook = strtr($file, '-', '_'); $implementations[$new_hook] = [ 'template' => $file, 'path' => dirname($files[$match]->uri), $arg_name => $info[$arg_name], 'base hook' => $base_hook, 'type' => 'theme_engine', 'theme path' => $theme_path, ]; if (isset($theme_registry[$hook]['preprocess functions'])) { $implementations[$new_hook]['preprocess functions'] = $theme_registry[$hook]['preprocess functions']; } } } } } if (!empty($implementations)) { $theme_registry += $implementations; } } // ------------------------------------------------------------------------ // End of workaround, hopefully we can kill this one day. // ------------------------------------------------------------------------. } /** * implements hook_blazy_alter(). */ function rocketship_core_blazy_alter(array &$build, array $settings = []) { // This will trigger when you use the Media Blazy formatter on a media reference field. // So, for example, on the Paragraph or Node level. This will do nothing if // you use the normal Blazy formatter on an Image field on the Media entity // level. // Only do something if media_switch set to content and it's a media item. if (!empty($settings['media_switch']) && $settings['media_switch'] == 'content' && isset($settings['media_source'])) { // Load the entity $entity = Drupal::entityTypeManager() ->getStorage($settings['entity_type_id']) ->load($settings['entity_id']); if ($entity) { // Get highest parent. Basically, if we loaded a Paragraph it'll // wind up at a non-Paragraph entity. If it's already a non-Paragraph // entity nothing will happen. $entity = Rocketship::getHighestLevelParentEntity($entity); /** @var \Drupal\Core\Entity\EntityRepositoryInterface $repo */ $repo = \Drupal::service('entity.repository'); $entity = $repo->getTranslationFromContext($entity); // Now fill that in as the content_url, Blazy will handle the rest. $build['#build']['settings']['content_url'] = $entity->toUrl(); } } } /** * Class Rocketship. * * @package Drupal\rocketship_core */ class Rocketship { public static function getHighestLevelParentEntity(EntityInterface $entity) { if (method_exists($entity, 'getParentEntity')) { $parent = $entity->getParentEntity(); if ($parent) { return static::getHighestLevelParentEntity($parent); } // Empty parent, assume this level is fine. return $entity; } // Already highest level as far as we can tell. return $entity; } /** * Update roles for a modules for Rocketship. * * If a module has permissions folder with .yml files * named ROLE.yml with a permissions key and then a list * of permissions, this function will update those roles * with those permissions. * * @param string $module * Module name. * * @throws \Drupal\Core\Entity\EntityStorageException */ public static function updateRolePermissionsForModule($module) { $path = \Drupal::service('module_handler') ->getModule($module) ->getPath() . '/permissions'; /** @var \Drupal\user\RoleInterface[] $roles */ $roles = Role::loadMultiple(); foreach ($roles as $role) { $file_path = $path . '/' . $role->id() . '.yml'; if (is_file($file_path)) { $file_contents = file_get_contents($file_path); $file = (array) Yaml::parse($file_contents); foreach ($file['permissions'] as $permission) { $role->grantPermission($permission); } $role->save(); } } } /** * Get module installer service. * * @return \Drupal\Core\Extension\ModuleInstallerInterface * Service. */ public static function getModuleInstaller() { return \Drupal::service('module_installer'); } /** * Get UUID Generator service. * * @return \Drupal\Component\Uuid\UuidInterface * Service. */ public static function getUuidGenerator() { return \Drupal::service('uuid'); } /** * Helper function to place blocks in a region. * * @param string $plugin_id * Block plugin ID. * @param array $settings * Block settings. * * @return bool|\Drupal\block\BlockInterface * FALSE on failure or the Block that was just placed. */ public static function placeBlock($plugin_id, array $settings = []) { // Set theme fallback. $theme = isset($settings['theme']) ? $settings['theme'] : \Drupal::configFactory() ->get('system.theme') ->get('default'); // Generate ID. $id = $theme . '_' . str_replace([':', '-'], '_', $plugin_id); if (strlen($id) > 64) { // Fallback if max length exceeded. $id = (new Random())->name(8); } // Make sure region is valid. $regions = system_region_list($theme); $region = isset($settings['region']) ? $settings['region'] : system_default_region($theme); if (!isset($regions[$region])) { $region = system_default_region($theme); } // Fill in defaults. $settings += [ 'plugin' => $plugin_id, 'region' => $region, 'id' => $id, 'theme' => $theme, 'label' => '', 'visibility' => [], 'weight' => 0, ]; $values = []; $keys = [ 'region', 'id', 'theme', 'plugin', 'weight', 'visibility', ]; foreach ($keys as $key) { $values[$key] = $settings[$key]; // Remove extra values that do not belong in the settings array. unset($settings[$key]); } foreach ($values['visibility'] as $id => $visibility) { $values['visibility'][$id]['id'] = $id; } $values['settings'] = $settings; $block = Block::create($values); try { $block->save(); return $block; } catch (\Exception $e) { \Drupal::logger('Rocketship')->error($e->getMessage()); return FALSE; } } /** * Render a block. * * @param string $plugin_id * Plugin ID. * @param array $config * Config array. * * @return array * Renderable array. * * @see https://drupal.stackexchange.com/questions/171686/how-can-i-programmatically-display-a-block */ public static function renderPluginBlock($plugin_id, array $config = []) { $block_manager = \Drupal::service('plugin.manager.block'); $plugin_block = $block_manager->createInstance($plugin_id, $config); // Some blocks might implement access check. $access_result = $plugin_block->access(\Drupal::currentUser()); // Return empty render array if user doesn't have access. // $access_result can be boolean or an AccessResult class. if (is_object($access_result) && $access_result->isForbidden() || is_bool($access_result) && !$access_result) { // You might need to add some cache tags/contexts. return []; } $render = $plugin_block->build(); // In some cases, you need to add the cache tags/context depending on // the block implemention. As it's possible to add the cache tags and // contexts in the render method and in ::getCacheTags and // ::getCacheContexts methods. return $render; } /** * Hides the breadcrumb and title block on detail pages for the given CT. * * @param string $theme * Theme name. * @param string $contentType * Content type name. * * @throws \Drupal\Core\Entity\EntityStorageException */ public static function hideBreadcrumbAndTitleBlockOnContentType($theme, $contentType) { $blocks = [ "{$theme}_breadcrumbs", "{$theme}_page_title", ]; foreach ($blocks as $config_name) { /** @var \Drupal\block\BlockInterface $block */ $block = Block::load($config_name); if ($block) { $visibility = $block->getVisibility(); $visibility['entity_bundle:node']['bundles'][$contentType] = $contentType; $visibility['entity_bundle:node']['negate'] = TRUE; $visibility['entity_bundle:node']['context_mapping']['node'] = '@node.node_route_context:node'; $block->setVisibilityConfig('entity_bundle:node', $visibility['entity_bundle:node']); $block->save(); } } } /** * @param string $entityType * Entity type name. * @param string $bundle * Bundle name. * * @deprecated * * Ensure content types have the translation tables. * * Creating CTs with config imports doesn't trigger translation table updates. * * @see https://www.drupal.org/project/drupal/issues/2599228 * * @todo: rework applyUpdates, because it's going away in 8.7 * This entire thing shouldn't be needed once 8.8 hits. * * The patch to fix translations has hit early, it's in 8.7 so this * method is no longer required. Marking as deprecated. */ public static function fixTranslationConfigImportIssues($entityType, $bundle) { } } if (!function_exists("array_key_last")) { function array_key_last($array) { if (!is_array($array) || empty($array)) { return NULL; } return array_keys($array)[count($array) - 1]; } }