diff --git a/composer.json b/composer.json index a54e303b5fc971ea87f1253e25591fa00f36a730..6cb11b7041d7b7c051100f06b80678fb211f4edb 100644 --- a/composer.json +++ b/composer.json @@ -281,7 +281,8 @@ "2811337": "https://www.drupal.org/files/issues/menu_block-2_level_menu_block_not_limited_to_active_parent-2811337-58.patch" }, "drupal/entity_embed": { - "2881745": "patches/entity_embed_2881745-22.patch" + "2881745": "patches/entity_embed_2881745-22.patch", + "2511404": "https://www.drupal.org/files/issues/2018-09-17/entity-embed-link-2511404-68.patch" }, "drupal/smtp": { "2781157": "https://www.drupal.org/files/issues/2018-11-07/2781157-n10.patch" diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 1e8f75e51e13f3ff673455d708ceb74747747d97..268745bd1052323533d000712264f4c8b4f3f518 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -3869,7 +3869,8 @@ } }, "patches_applied": { - "2881745": "patches/entity_embed_2881745-22.patch" + "2881745": "patches/entity_embed_2881745-22.patch", + "2511404": "https://www.drupal.org/files/issues/2018-09-17/entity-embed-link-2511404-68.patch" } }, "installation-source": "dist", diff --git a/web/modules/entity_embed/entity_embed.module b/web/modules/entity_embed/entity_embed.module index fbdd99434e5d9694a35bf3d6930f13becf2ba9ca..079bc82a615f91877bcebe32b70ca5a75521940c 100644 --- a/web/modules/entity_embed/entity_embed.module +++ b/web/modules/entity_embed/entity_embed.module @@ -6,6 +6,9 @@ * format. */ +use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Url; + /** * Implements hook_theme(). */ @@ -31,6 +34,25 @@ function template_preprocess_entity_embed_container(&$variables) { $variables['element'] += ['#attributes' => []]; $variables['attributes'] = $variables['element']['#attributes']; $variables['children'] = $variables['element']['#children']; + if (!empty($variables['element']['#context']['data-entity-embed-display-settings']['link_url'])) { + $link = UrlHelper::filterBadProtocol($variables['element']['#context']['data-entity-embed-display-settings']['link_url']); + if (!UrlHelper::isExternal($link)) { + $link = 'internal:/' . ltrim($link, '/'); + } + $link = Url::fromUri($link); + $attributes = []; + if (!empty($variables['element']['#context']['data-entity-embed-display-settings']['link_url_target']) && $variables['element']['#context']['data-entity-embed-display-settings']['link_url_target'] == 1) { + $attributes = ['attributes' => ['target' => '_blank']]; + } + $variables['children'] = [ + [ + '#type' => 'link', + '#title' => $variables['children'], + '#options' => $attributes, + '#url' => $link, + ] + ]; + } } /** diff --git a/web/modules/entity_embed/src/Form/EntityEmbedDialog.php b/web/modules/entity_embed/src/Form/EntityEmbedDialog.php index ae0194b3879e667878f6cdbf1213a59b3375c6c6..a26cbc2ece691f22ad55c9098a25ffa4a128d059 100644 --- a/web/modules/entity_embed/src/Form/EntityEmbedDialog.php +++ b/web/modules/entity_embed/src/Form/EntityEmbedDialog.php @@ -8,6 +8,7 @@ use Drupal\Core\Ajax\CloseModalDialogCommand; use Drupal\Core\Ajax\HtmlCommand; use Drupal\Core\Ajax\SetDialogTitleCommand; +use Drupal\Core\Entity\Element\EntityAutocomplete; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -434,6 +435,31 @@ public function buildEmbedStep(array $form, FormStateInterface $form_state) { if (is_string($entity_element['data-entity-embed-display-settings'])) { $entity_element['data-entity-embed-display-settings'] = Json::decode($entity_element['data-entity-embed-display-settings']); } + + // Supress Drupal's "Link image to" dropdown when embedding an image, + // since the 'Link to' option provides this functionality. + if (isset($form['attributes']['data-entity-embed-display-settings']['image_link'])) { + $form['attributes']['data-entity-embed-display-settings']['image_link']['#type'] = 'hidden'; + $form['attributes']['data-entity-embed-display-settings']['image_link']['#value'] = ''; + } + $form['attributes']['data-entity-embed-display-settings']['link_url'] = [ + '#title' => t('Link to'), + '#type' => 'entity_autocomplete', + '#target_type' => 'node', + '#attributes' => [ + 'data-autocomplete-first-character-blacklist' => '/#?' + ], + '#element_validate' => [[get_called_class(), 'validateUriElement']], + '#process_default_value' => FALSE, + '#description' => $this->t('Start typing the title of a piece of content to select it. You can also enter an internal path such as %add-node or an external URL such as %url. Enter %front to link to the front page.', ['%front' => '<front>', '%add-node' => '/node/add', '%url' => 'http://example.com']), + '#default_value' => isset($entity_element['data-entity-embed-display-settings']['link_url']) ? $this->getUriAsDisplayableString($entity_element['data-entity-embed-display-settings']['link_url']) : '', + '#maxlength' => 2048, + ]; + $form['attributes']['data-entity-embed-display-settings']['link_url_target'] = [ + '#title' => t('Open in a new window?'), + '#type' => 'checkbox', + '#default_value' => isset($entity_element['data-entity-embed-display-settings']['link_url_target']) ? Html::decodeEntities($entity_element['data-entity-embed-display-settings']['link_url_target']) : '', + ]; $display = $this->entityEmbedDisplayManager->createInstance($plugin_id, $entity_element['data-entity-embed-display-settings']); $display->setContextValue('entity', $entity); $display->setAttributes($entity_element); @@ -497,6 +523,118 @@ public function buildEmbedStep(array $form, FormStateInterface $form_state) { return $form; } + /** + * Gets the URI without the 'internal:' or 'entity:' scheme. + * + * The following two forms of URIs are transformed: + * - 'entity:' URIs: to entity autocomplete ("label (entity id)") strings; + * - 'internal:' URIs: the scheme is stripped. + * + * This method is the inverse of ::getUserEnteredStringAsUri(). + * + * @param string $uri + * The URI to get the displayable string for. + * + * @return string + * + * @see static::getUserEnteredStringAsUri() + */ + protected function getUriAsDisplayableString($uri) { + $uri = Html::decodeEntities($uri); + $scheme = parse_url($uri, PHP_URL_SCHEME); + + // By default, the displayable string is the URI. + $displayable_string = $uri; + + // A different displayable string may be chosen in case of the 'internal:' + // or 'entity:' built-in schemes. + if ($scheme === 'internal') { + $uri_reference = explode(':', $uri, 2)[1]; + + // @todo '<front>' is valid input for BC reasons, may be removed by + // https://www.drupal.org/node/2421941 + $path = parse_url($uri, PHP_URL_PATH); + if ($path === '/') { + $uri_reference = '<front>' . substr($uri_reference, 1); + } + + $displayable_string = $uri_reference; + } + elseif ($scheme === 'entity') { + list($entity_type, $entity_id) = explode('/', substr($uri, 7), 2); + // Show the 'entity:' URI as the entity autocomplete would. + // @todo Support entity types other than 'node'. Will be fixed in + // https://www.drupal.org/node/2423093. + if ($entity_type == 'node' && $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity_id)) { + $displayable_string = EntityAutocomplete::getEntityLabels([$entity]); + } + } + + return $displayable_string; + } + + /** + * Gets the user-entered string as a URI. + * + * The following two forms of input are mapped to URIs: + * - entity autocomplete ("label (entity id)") strings: to 'entity:' URIs; + * - strings without a detectable scheme: to 'internal:' URIs. + * + * This method is the inverse of ::getUriAsDisplayableString(). + * + * @param string $string + * The user-entered string. + * + * @return string + * The URI, if a non-empty $uri was passed. + * + * @see static::getUriAsDisplayableString() + */ + protected static function getUserEnteredStringAsUri($string) { + // By default, assume the entered string is an URI. + $uri = $string; + + // Detect entity autocomplete string, map to 'entity:' URI. + $entity_id = EntityAutocomplete::extractEntityIdFromAutocompleteInput($string); + if ($entity_id !== NULL) { + // @todo Support entity types other than 'node'. Will be fixed in + // https://www.drupal.org/node/2423093. + $uri = 'entity:node/' . $entity_id; + } + // Detect a schemeless string, map to 'internal:' URI. + elseif (!empty($string) && parse_url($string, PHP_URL_SCHEME) === NULL) { + // @todo '<front>' is valid input for BC reasons, may be removed by + // https://www.drupal.org/node/2421941 + // - '<front>' -> '/' + // - '<front>#foo' -> '/#foo' + if (strpos($string, '<front>') === 0) { + $string = '/' . substr($string, strlen('<front>')); + } + $uri = 'internal:' . $string; + } + + return $uri; + } + + /** + * Form element validation handler for the 'uri' element. + * + * Disallows saving inaccessible or untrusted URLs. + */ + public static function validateUriElement($element, FormStateInterface $form_state, $form) { + $uri = static::getUserEnteredStringAsUri($element['#value']); + $form_state->setValueForElement($element, $uri); + + // If getUserEnteredStringAsUri() mapped the entered value to a 'internal:' + // URI , ensure the raw value begins with '/', '?' or '#'. + // @todo '<front>' is valid input for BC reasons, may be removed by + // https://www.drupal.org/node/2421941 + if (parse_url($uri, PHP_URL_SCHEME) === 'internal' && !in_array($element['#value'][0], ['/', '?', '#'], TRUE) && substr($element['#value'], 0, 7) !== '<front>') { + $form_state->setError($element, t('Manually entered paths should start with /, ? or #.')); + return; + } + } + /** * {@inheritdoc} */