"Fossies" - the Fresh Open Source Software Archive

Member "drupal-8.9.10/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php" (26 Nov 2020, 17638 Bytes) of package /linux/www/drupal-8.9.10.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "LinkWidget.php" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 9.0.8_vs_9.1.0-rc1.

    1 <?php
    2 
    3 namespace Drupal\link\Plugin\Field\FieldWidget;
    4 
    5 use Drupal\Core\Url;
    6 use Drupal\Core\Entity\Element\EntityAutocomplete;
    7 use Drupal\Core\Field\FieldItemListInterface;
    8 use Drupal\Core\Field\WidgetBase;
    9 use Drupal\Core\Form\FormStateInterface;
   10 use Drupal\link\LinkItemInterface;
   11 use Symfony\Component\Validator\ConstraintViolation;
   12 use Symfony\Component\Validator\ConstraintViolationListInterface;
   13 
   14 /**
   15  * Plugin implementation of the 'link' widget.
   16  *
   17  * @FieldWidget(
   18  *   id = "link_default",
   19  *   label = @Translation("Link"),
   20  *   field_types = {
   21  *     "link"
   22  *   }
   23  * )
   24  */
   25 class LinkWidget extends WidgetBase {
   26 
   27   /**
   28    * {@inheritdoc}
   29    */
   30   public static function defaultSettings() {
   31     return [
   32       'placeholder_url' => '',
   33       'placeholder_title' => '',
   34     ] + parent::defaultSettings();
   35   }
   36 
   37   /**
   38    * Gets the URI without the 'internal:' or 'entity:' scheme.
   39    *
   40    * The following two forms of URIs are transformed:
   41    * - 'entity:' URIs: to entity autocomplete ("label (entity id)") strings;
   42    * - 'internal:' URIs: the scheme is stripped.
   43    *
   44    * This method is the inverse of ::getUserEnteredStringAsUri().
   45    *
   46    * @param string $uri
   47    *   The URI to get the displayable string for.
   48    *
   49    * @return string
   50    *
   51    * @see static::getUserEnteredStringAsUri()
   52    */
   53   protected static function getUriAsDisplayableString($uri) {
   54     $scheme = parse_url($uri, PHP_URL_SCHEME);
   55 
   56     // By default, the displayable string is the URI.
   57     $displayable_string = $uri;
   58 
   59     // A different displayable string may be chosen in case of the 'internal:'
   60     // or 'entity:' built-in schemes.
   61     if ($scheme === 'internal') {
   62       $uri_reference = explode(':', $uri, 2)[1];
   63 
   64       // @todo '<front>' is valid input for BC reasons, may be removed by
   65       //   https://www.drupal.org/node/2421941
   66       $path = parse_url($uri, PHP_URL_PATH);
   67       if ($path === '/') {
   68         $uri_reference = '<front>' . substr($uri_reference, 1);
   69       }
   70 
   71       $displayable_string = $uri_reference;
   72     }
   73     elseif ($scheme === 'entity') {
   74       list($entity_type, $entity_id) = explode('/', substr($uri, 7), 2);
   75       // Show the 'entity:' URI as the entity autocomplete would.
   76       // @todo Support entity types other than 'node'. Will be fixed in
   77       //    https://www.drupal.org/node/2423093.
   78       if ($entity_type == 'node' && $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity_id)) {
   79         $displayable_string = EntityAutocomplete::getEntityLabels([$entity]);
   80       }
   81     }
   82     elseif ($scheme === 'route') {
   83       $displayable_string = ltrim($displayable_string, 'route:');
   84     }
   85 
   86     return $displayable_string;
   87   }
   88 
   89   /**
   90    * Gets the user-entered string as a URI.
   91    *
   92    * The following two forms of input are mapped to URIs:
   93    * - entity autocomplete ("label (entity id)") strings: to 'entity:' URIs;
   94    * - strings without a detectable scheme: to 'internal:' URIs.
   95    *
   96    * This method is the inverse of ::getUriAsDisplayableString().
   97    *
   98    * @param string $string
   99    *   The user-entered string.
  100    *
  101    * @return string
  102    *   The URI, if a non-empty $uri was passed.
  103    *
  104    * @see static::getUriAsDisplayableString()
  105    */
  106   protected static function getUserEnteredStringAsUri($string) {
  107     // By default, assume the entered string is an URI.
  108     $uri = trim($string);
  109 
  110     // Detect entity autocomplete string, map to 'entity:' URI.
  111     $entity_id = EntityAutocomplete::extractEntityIdFromAutocompleteInput($string);
  112     if ($entity_id !== NULL) {
  113       // @todo Support entity types other than 'node'. Will be fixed in
  114       //    https://www.drupal.org/node/2423093.
  115       $uri = 'entity:node/' . $entity_id;
  116     }
  117     // Support linking to nothing.
  118     elseif (in_array($string, ['<nolink>', '<none>'], TRUE)) {
  119       $uri = 'route:' . $string;
  120     }
  121     // Detect a schemeless string, map to 'internal:' URI.
  122     elseif (!empty($string) && parse_url($string, PHP_URL_SCHEME) === NULL) {
  123       // @todo '<front>' is valid input for BC reasons, may be removed by
  124       //   https://www.drupal.org/node/2421941
  125       // - '<front>' -> '/'
  126       // - '<front>#foo' -> '/#foo'
  127       if (strpos($string, '<front>') === 0) {
  128         $string = '/' . substr($string, strlen('<front>'));
  129       }
  130       $uri = 'internal:' . $string;
  131     }
  132 
  133     return $uri;
  134   }
  135 
  136   /**
  137    * Form element validation handler for the 'uri' element.
  138    *
  139    * Disallows saving inaccessible or untrusted URLs.
  140    */
  141   public static function validateUriElement($element, FormStateInterface $form_state, $form) {
  142     $uri = static::getUserEnteredStringAsUri($element['#value']);
  143     $form_state->setValueForElement($element, $uri);
  144 
  145     // If getUserEnteredStringAsUri() mapped the entered value to a 'internal:'
  146     // URI , ensure the raw value begins with '/', '?' or '#'.
  147     // @todo '<front>' is valid input for BC reasons, may be removed by
  148     //   https://www.drupal.org/node/2421941
  149     if (parse_url($uri, PHP_URL_SCHEME) === 'internal' && !in_array($element['#value'][0], ['/', '?', '#'], TRUE) && substr($element['#value'], 0, 7) !== '<front>') {
  150       $form_state->setError($element, t('Manually entered paths should start with one of the following characters: / ? #'));
  151       return;
  152     }
  153   }
  154 
  155   /**
  156    * Form element validation handler for the 'title' element.
  157    *
  158    * Conditionally requires the link title if a URL value was filled in.
  159    */
  160   public static function validateTitleElement(&$element, FormStateInterface $form_state, $form) {
  161     if ($element['uri']['#value'] !== '' && $element['title']['#value'] === '') {
  162       // We expect the field name placeholder value to be wrapped in t() here,
  163       // so it won't be escaped again as it's already marked safe.
  164       $form_state->setError($element['title'], t('@title field is required if there is @uri input.', ['@title' => $element['title']['#title'], '@uri' => $element['uri']['#title']]));
  165     }
  166   }
  167 
  168   /**
  169    * Form element validation handler for the 'title' element.
  170    *
  171    * Requires the URL value if a link title was filled in.
  172    */
  173   public static function validateTitleNoLink(&$element, FormStateInterface $form_state, $form) {
  174     if ($element['uri']['#value'] === '' && $element['title']['#value'] !== '') {
  175       $form_state->setError($element['uri'], t('The @uri field is required when the @title field is specified.', ['@title' => $element['title']['#title'], '@uri' => $element['uri']['#title']]));
  176     }
  177   }
  178 
  179   /**
  180    * {@inheritdoc}
  181    */
  182   public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
  183     /** @var \Drupal\link\LinkItemInterface $item */
  184     $item = $items[$delta];
  185 
  186     $element['uri'] = [
  187       '#type' => 'url',
  188       '#title' => $this->t('URL'),
  189       '#placeholder' => $this->getSetting('placeholder_url'),
  190       // The current field value could have been entered by a different user.
  191       // However, if it is inaccessible to the current user, do not display it
  192       // to them.
  193       '#default_value' => (!$item->isEmpty() && (\Drupal::currentUser()->hasPermission('link to any page') || $item->getUrl()->access())) ? static::getUriAsDisplayableString($item->uri) : NULL,
  194       '#element_validate' => [[get_called_class(), 'validateUriElement']],
  195       '#maxlength' => 2048,
  196       '#required' => $element['#required'],
  197       '#link_type' => $this->getFieldSetting('link_type'),
  198     ];
  199 
  200     // If the field is configured to support internal links, it cannot use the
  201     // 'url' form element and we have to do the validation ourselves.
  202     if ($this->supportsInternalLinks()) {
  203       $element['uri']['#type'] = 'entity_autocomplete';
  204       // @todo The user should be able to select an entity type. Will be fixed
  205       //    in https://www.drupal.org/node/2423093.
  206       $element['uri']['#target_type'] = 'node';
  207       // Disable autocompletion when the first character is '/', '#' or '?'.
  208       $element['uri']['#attributes']['data-autocomplete-first-character-blacklist'] = '/#?';
  209 
  210       // The link widget is doing its own processing in
  211       // static::getUriAsDisplayableString().
  212       $element['uri']['#process_default_value'] = FALSE;
  213     }
  214 
  215     // If the field is configured to allow only internal links, add a useful
  216     // element prefix and description.
  217     if (!$this->supportsExternalLinks()) {
  218       $element['uri']['#field_prefix'] = rtrim(Url::fromRoute('<front>', [], ['absolute' => TRUE])->toString(), '/');
  219       $element['uri']['#description'] = $this->t('This must be an internal path such as %add-node. You can also start typing the title of a piece of content to select it. Enter %front to link to the front page. Enter %nolink to display link text only.', ['%add-node' => '/node/add', '%front' => '<front>', '%nolink' => '<nolink>']);
  220     }
  221     // If the field is configured to allow both internal and external links,
  222     // show a useful description.
  223     elseif ($this->supportsExternalLinks() && $this->supportsInternalLinks()) {
  224       $element['uri']['#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. Enter %nolink to display link text only.', ['%front' => '<front>', '%add-node' => '/node/add', '%url' => 'http://example.com', '%nolink' => '<nolink>']);
  225     }
  226     // If the field is configured to allow only external links, show a useful
  227     // description.
  228     elseif ($this->supportsExternalLinks() && !$this->supportsInternalLinks()) {
  229       $element['uri']['#description'] = $this->t('This must be an external URL such as %url.', ['%url' => 'http://example.com']);
  230     }
  231 
  232     $element['title'] = [
  233       '#type' => 'textfield',
  234       '#title' => $this->t('Link text'),
  235       '#placeholder' => $this->getSetting('placeholder_title'),
  236       '#default_value' => isset($items[$delta]->title) ? $items[$delta]->title : NULL,
  237       '#maxlength' => 255,
  238       '#access' => $this->getFieldSetting('title') != DRUPAL_DISABLED,
  239       '#required' => $this->getFieldSetting('title') === DRUPAL_REQUIRED && $element['#required'],
  240     ];
  241     // Post-process the title field to make it conditionally required if URL is
  242     // non-empty. Omit the validation on the field edit form, since the field
  243     // settings cannot be saved otherwise.
  244     //
  245     // Validate that title field is filled out (regardless of uri) when it is a
  246     // required field.
  247     if (!$this->isDefaultValueWidget($form_state) && $this->getFieldSetting('title') === DRUPAL_REQUIRED) {
  248       $element['#element_validate'][] = [get_called_class(), 'validateTitleElement'];
  249       $element['#element_validate'][] = [get_called_class(), 'validateTitleNoLink'];
  250 
  251       if (!$element['title']['#required']) {
  252         // Make title required on the front-end when URI filled-in.
  253         $field_name = $this->fieldDefinition->getName();
  254 
  255         $parents = $element['#field_parents'];
  256         $parents[] = $field_name;
  257         $selector = $root = array_shift($parents);
  258         if ($parents) {
  259           $selector = $root . '[' . implode('][', $parents) . ']';
  260         }
  261 
  262         $element['title']['#states']['required'] = [
  263           ':input[name="' . $selector . '[' . $delta . '][uri]"]' => ['filled' => TRUE],
  264         ];
  265       }
  266     }
  267 
  268     // Ensure that a URI is always entered when an optional title field is
  269     // submitted.
  270     if (!$this->isDefaultValueWidget($form_state) && $this->getFieldSetting('title') == DRUPAL_OPTIONAL) {
  271       $element['#element_validate'][] = [get_called_class(), 'validateTitleNoLink'];
  272     }
  273 
  274     // Exposing the attributes array in the widget is left for alternate and more
  275     // advanced field widgets.
  276     $element['attributes'] = [
  277       '#type' => 'value',
  278       '#tree' => TRUE,
  279       '#value' => !empty($items[$delta]->options['attributes']) ? $items[$delta]->options['attributes'] : [],
  280       '#attributes' => ['class' => ['link-field-widget-attributes']],
  281     ];
  282 
  283     // If cardinality is 1, ensure a proper label is output for the field.
  284     if ($this->fieldDefinition->getFieldStorageDefinition()->getCardinality() == 1) {
  285       // If the link title is disabled, use the field definition label as the
  286       // title of the 'uri' element.
  287       if ($this->getFieldSetting('title') == DRUPAL_DISABLED) {
  288         $element['uri']['#title'] = $element['#title'];
  289         // By default the field description is added to the title field. Since
  290         // the title field is disabled, we add the description, if given, to the
  291         // uri element instead.
  292         if (!empty($element['#description'])) {
  293           if (empty($element['uri']['#description'])) {
  294             $element['uri']['#description'] = $element['#description'];
  295           }
  296           else {
  297             // If we have the description of the type of field together with
  298             // the user provided description, we want to make a distinction
  299             // between "core help text" and "user entered help text". To make
  300             // this distinction more clear, we put them in an unordered list.
  301             $element['uri']['#description'] = [
  302               '#theme' => 'item_list',
  303               '#items' => [
  304                 // Assume the user-specified description has the most relevance,
  305                 // so place it first.
  306                 $element['#description'],
  307                 $element['uri']['#description'],
  308               ],
  309             ];
  310           }
  311         }
  312       }
  313       // Otherwise wrap everything in a details element.
  314       else {
  315         $element += [
  316           '#type' => 'fieldset',
  317         ];
  318       }
  319     }
  320 
  321     return $element;
  322   }
  323 
  324   /**
  325    * Indicates enabled support for link to routes.
  326    *
  327    * @return bool
  328    *   Returns TRUE if the LinkItem field is configured to support links to
  329    *   routes, otherwise FALSE.
  330    */
  331   protected function supportsInternalLinks() {
  332     $link_type = $this->getFieldSetting('link_type');
  333     return (bool) ($link_type & LinkItemInterface::LINK_INTERNAL);
  334   }
  335 
  336   /**
  337    * Indicates enabled support for link to external URLs.
  338    *
  339    * @return bool
  340    *   Returns TRUE if the LinkItem field is configured to support links to
  341    *   external URLs, otherwise FALSE.
  342    */
  343   protected function supportsExternalLinks() {
  344     $link_type = $this->getFieldSetting('link_type');
  345     return (bool) ($link_type & LinkItemInterface::LINK_EXTERNAL);
  346   }
  347 
  348   /**
  349    * {@inheritdoc}
  350    */
  351   public function settingsForm(array $form, FormStateInterface $form_state) {
  352     $elements = parent::settingsForm($form, $form_state);
  353 
  354     $elements['placeholder_url'] = [
  355       '#type' => 'textfield',
  356       '#title' => $this->t('Placeholder for URL'),
  357       '#default_value' => $this->getSetting('placeholder_url'),
  358       '#description' => $this->t('Text that will be shown inside the field until a value is entered. This hint is usually a sample value or a brief description of the expected format.'),
  359     ];
  360     $elements['placeholder_title'] = [
  361       '#type' => 'textfield',
  362       '#title' => $this->t('Placeholder for link text'),
  363       '#default_value' => $this->getSetting('placeholder_title'),
  364       '#description' => $this->t('Text that will be shown inside the field until a value is entered. This hint is usually a sample value or a brief description of the expected format.'),
  365       '#states' => [
  366         'invisible' => [
  367           ':input[name="instance[settings][title]"]' => ['value' => DRUPAL_DISABLED],
  368         ],
  369       ],
  370     ];
  371 
  372     return $elements;
  373   }
  374 
  375   /**
  376    * {@inheritdoc}
  377    */
  378   public function settingsSummary() {
  379     $summary = [];
  380 
  381     $placeholder_title = $this->getSetting('placeholder_title');
  382     $placeholder_url = $this->getSetting('placeholder_url');
  383     if (empty($placeholder_title) && empty($placeholder_url)) {
  384       $summary[] = $this->t('No placeholders');
  385     }
  386     else {
  387       if (!empty($placeholder_title)) {
  388         $summary[] = $this->t('Title placeholder: @placeholder_title', ['@placeholder_title' => $placeholder_title]);
  389       }
  390       if (!empty($placeholder_url)) {
  391         $summary[] = $this->t('URL placeholder: @placeholder_url', ['@placeholder_url' => $placeholder_url]);
  392       }
  393     }
  394 
  395     return $summary;
  396   }
  397 
  398   /**
  399    * {@inheritdoc}
  400    */
  401   public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
  402     foreach ($values as &$value) {
  403       $value['uri'] = static::getUserEnteredStringAsUri($value['uri']);
  404       $value += ['options' => []];
  405     }
  406     return $values;
  407   }
  408 
  409   /**
  410    * {@inheritdoc}
  411    *
  412    * Override the '%uri' message parameter, to ensure that 'internal:' URIs
  413    * show a validation error message that doesn't mention that scheme.
  414    */
  415   public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
  416     /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
  417     foreach ($violations as $offset => $violation) {
  418       $parameters = $violation->getParameters();
  419       if (isset($parameters['@uri'])) {
  420         $parameters['@uri'] = static::getUriAsDisplayableString($parameters['@uri']);
  421         $violations->set($offset, new ConstraintViolation(
  422           $this->t($violation->getMessageTemplate(), $parameters),
  423           $violation->getMessageTemplate(),
  424           $parameters,
  425           $violation->getRoot(),
  426           $violation->getPropertyPath(),
  427           $violation->getInvalidValue(),
  428           $violation->getPlural(),
  429           $violation->getCode()
  430         ));
  431       }
  432     }
  433     parent::flagErrors($items, $violations, $form, $form_state);
  434   }
  435 
  436 }