diff --git a/entity_embed.module b/entity_embed.module index fbdd994..079bc82 100644 --- a/entity_embed.module +++ b/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/src/Form/EntityEmbedDialog.php b/src/Form/EntityEmbedDialog.php index ae0194b..a26cbc2 100644 --- a/src/Form/EntityEmbedDialog.php +++ b/src/Form/EntityEmbedDialog.php @@ -8,6 +8,7 @@ use Drupal\Core\Ajax\AjaxResponse; 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 @@ class EntityEmbedDialog extends FormBase { 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 @@ class EntityEmbedDialog extends FormBase { 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} */