Chris@0: " of an Chris@0: * HTML tag, such as in HTML attribute values. This would be a security Chris@0: * risk. Examples: Chris@0: * @code Chris@0: * // Insecure (placeholder within "<" and ">"): Chris@0: * $this->placeholderFormat('<@variable>text', ['@variable' => $variable]); Chris@0: * // Insecure (placeholder within "<" and ">"): Chris@0: * $this->placeholderFormat('link text', ['@variable' => $variable]); Chris@0: * // Insecure (placeholder within "<" and ">"): Chris@0: * $this->placeholderFormat('link text', ['@variable' => $variable]); Chris@0: * @endcode Chris@0: * Only the "href" attribute is supported via the special ":variable" Chris@0: * placeholder, to allow simple links to be inserted: Chris@0: * @code Chris@0: * // Secure (usage of ":variable" placeholder for href attribute): Chris@0: * $this->placeholderFormat('link text', [':variable' , $variable]); Chris@0: * // Secure (usage of ":variable" placeholder for href attribute): Chris@0: * $this->placeholderFormat('link text', [':variable' => $variable]); Chris@0: * // Insecure (the "@variable" placeholder does not filter dangerous Chris@0: * // protocols): Chris@0: * $this->placeholderFormat('link text', ['@variable' => $variable]); Chris@0: * // Insecure ("@variable" placeholder within "<" and ">"): Chris@0: * $this->placeholderFormat('link text', [':url' => $url, '@variable' => $variable]); Chris@0: * @endcode Chris@0: * To build non-minimal HTML, use an HTML template language such as Twig, Chris@0: * rather than this class. Chris@0: * Chris@0: * @ingroup sanitization Chris@0: * Chris@0: * @see \Drupal\Core\StringTranslation\TranslatableMarkup Chris@0: * @see \Drupal\Core\StringTranslation\PluralTranslatableMarkup Chris@0: * @see \Drupal\Component\Render\FormattableMarkup::placeholderFormat() Chris@0: */ Chris@0: class FormattableMarkup implements MarkupInterface, \Countable { Chris@0: Chris@0: /** Chris@14: * The string containing placeholders. Chris@14: * Chris@14: * @var string Chris@14: */ Chris@14: protected $string; Chris@14: Chris@14: /** Chris@0: * The arguments to replace placeholders with. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: protected $arguments = []; Chris@0: Chris@0: /** Chris@0: * Constructs a new class instance. Chris@0: * Chris@0: * @param string $string Chris@0: * A string containing placeholders. The string itself will not be escaped, Chris@0: * any unsafe content must be in $args and inserted via placeholders. Chris@0: * @param array $arguments Chris@0: * An array with placeholder replacements, keyed by placeholder. See Chris@0: * \Drupal\Component\Render\FormattableMarkup::placeholderFormat() for Chris@0: * additional information about placeholders. Chris@0: * Chris@0: * @see \Drupal\Component\Render\FormattableMarkup::placeholderFormat() Chris@0: */ Chris@0: public function __construct($string, array $arguments) { Chris@0: $this->string = (string) $string; Chris@0: $this->arguments = $arguments; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function __toString() { Chris@0: return static::placeholderFormat($this->string, $this->arguments); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the string length. Chris@0: * Chris@0: * @return int Chris@0: * The length of the string. Chris@0: */ Chris@0: public function count() { Chris@17: return mb_strlen($this->string); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns a representation of the object for use in JSON serialization. Chris@0: * Chris@0: * @return string Chris@0: * The safe string content. Chris@0: */ Chris@0: public function jsonSerialize() { Chris@0: return $this->__toString(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Replaces placeholders in a string with values. Chris@0: * Chris@0: * @param string $string Chris@0: * A string containing placeholders. The string itself is expected to be Chris@0: * safe and correct HTML. Any unsafe content must be in $args and Chris@0: * inserted via placeholders. Chris@0: * @param array $args Chris@0: * An associative array of replacements. Each array key should be the same Chris@0: * as a placeholder in $string. The corresponding value should be a string Chris@0: * or an object that implements Chris@0: * \Drupal\Component\Render\MarkupInterface. The value replaces the Chris@0: * placeholder in $string. Sanitization and formatting will be done before Chris@0: * replacement. The type of sanitization and formatting depends on the first Chris@0: * character of the key: Chris@0: * - @variable: When the placeholder replacement value is: Chris@0: * - A string, the replaced value in the returned string will be sanitized Chris@0: * using \Drupal\Component\Utility\Html::escape(). Chris@0: * - A MarkupInterface object, the replaced value in the returned string Chris@0: * will not be sanitized. Chris@0: * - A MarkupInterface object cast to a string, the replaced value in the Chris@0: * returned string be forcibly sanitized using Chris@0: * \Drupal\Component\Utility\Html::escape(). Chris@0: * @code Chris@0: * $this->placeholderFormat('This will force HTML-escaping of the replacement value: @text', ['@text' => (string) $safe_string_interface_object)); Chris@0: * @endcode Chris@0: * Use this placeholder as the default choice for anything displayed on Chris@0: * the site, but not within HTML attributes, JavaScript, or CSS. Doing so Chris@0: * is a security risk. Chris@0: * - %variable: Use when the replacement value is to be wrapped in Chris@0: * tags. Chris@0: * A call like: Chris@0: * @code Chris@0: * $string = "%output_text"; Chris@0: * $arguments = ['%output_text' => 'text output here.']; Chris@0: * $this->placeholderFormat($string, $arguments); Chris@0: * @endcode Chris@0: * makes the following HTML code: Chris@0: * @code Chris@0: * text output here. Chris@0: * @endcode Chris@0: * As with @variable, do not use this within HTML attributes, JavaScript, Chris@0: * or CSS. Doing so is a security risk. Chris@0: * - :variable: Return value is escaped with Chris@0: * \Drupal\Component\Utility\Html::escape() and filtered for dangerous Chris@0: * protocols using UrlHelper::stripDangerousProtocols(). Use this when Chris@0: * using the "href" attribute, ensuring the attribute value is always Chris@0: * wrapped in quotes: Chris@0: * @code Chris@0: * // Secure (with quotes): Chris@0: * $this->placeholderFormat('@variable', [':url' => $url, '@variable' => $variable]); Chris@0: * // Insecure (without quotes): Chris@0: * $this->placeholderFormat('@variable', [':url' => $url, '@variable' => $variable]); Chris@0: * @endcode Chris@0: * When ":variable" comes from arbitrary user input, the result is secure, Chris@0: * but not guaranteed to be a valid URL (which means the resulting output Chris@0: * could fail HTML validation). To guarantee a valid URL, use Chris@0: * Url::fromUri($user_input)->toString() (which either throws an exception Chris@0: * or returns a well-formed URL) before passing the result into a Chris@0: * ":variable" placeholder. Chris@0: * Chris@0: * @return string Chris@0: * A formatted HTML string with the placeholders replaced. Chris@0: * Chris@0: * @ingroup sanitization Chris@0: * Chris@0: * @see \Drupal\Core\StringTranslation\TranslatableMarkup Chris@0: * @see \Drupal\Core\StringTranslation\PluralTranslatableMarkup Chris@0: * @see \Drupal\Component\Utility\Html::escape() Chris@0: * @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols() Chris@0: * @see \Drupal\Core\Url::fromUri() Chris@0: */ Chris@0: protected static function placeholderFormat($string, array $args) { Chris@0: // Transform arguments before inserting them. Chris@0: foreach ($args as $key => $value) { Chris@0: switch ($key[0]) { Chris@0: case '@': Chris@0: // Escape if the value is not an object from a class that implements Chris@0: // \Drupal\Component\Render\MarkupInterface, for example strings will Chris@0: // be escaped. Chris@0: // Strings that are safe within HTML fragments, but not within other Chris@0: // contexts, may still be an instance of Chris@0: // \Drupal\Component\Render\MarkupInterface, so this placeholder type Chris@0: // must not be used within HTML attributes, JavaScript, or CSS. Chris@0: $args[$key] = static::placeholderEscape($value); Chris@0: break; Chris@0: Chris@0: case ':': Chris@0: // Strip URL protocols that can be XSS vectors. Chris@0: $value = UrlHelper::stripDangerousProtocols($value); Chris@0: // Escape unconditionally, without checking whether the value is an Chris@0: // instance of \Drupal\Component\Render\MarkupInterface. This forces Chris@0: // characters that are unsafe for use in an "href" HTML attribute to Chris@0: // be encoded. If a caller wants to pass a value that is extracted Chris@0: // from HTML and therefore is already HTML encoded, it must invoke Chris@0: // \Drupal\Component\Render\OutputStrategyInterface::renderFromHtml() Chris@0: // on it prior to passing it in as a placeholder value of this type. Chris@0: // @todo Add some advice and stronger warnings. Chris@0: // https://www.drupal.org/node/2569041. Chris@0: $args[$key] = Html::escape($value); Chris@0: break; Chris@0: Chris@0: case '%': Chris@0: // Similarly to @, escape non-safe values. Also, add wrapping markup Chris@0: // in order to render as a placeholder. Not for use within attributes, Chris@0: // per the warning above about Chris@0: // \Drupal\Component\Render\MarkupInterface and also due to the Chris@0: // wrapping markup. Chris@0: $args[$key] = '' . static::placeholderEscape($value) . ''; Chris@0: break; Chris@0: Chris@0: default: Chris@0: // We do not trigger an error for placeholder that start with an Chris@0: // alphabetic character. Chris@0: // @todo https://www.drupal.org/node/2807743 Change to an exception Chris@0: // and always throw regardless of the first character. Chris@0: if (!ctype_alpha($key[0])) { Chris@0: // We trigger an error as we may want to introduce new placeholders Chris@0: // in the future without breaking backward compatibility. Chris@0: trigger_error('Invalid placeholder (' . $key . ') in string: ' . $string, E_USER_ERROR); Chris@0: } Chris@0: elseif (strpos($string, $key) !== FALSE) { Chris@0: trigger_error('Invalid placeholder (' . $key . ') in string: ' . $string, E_USER_DEPRECATED); Chris@0: } Chris@0: // No replacement possible therefore we can discard the argument. Chris@0: unset($args[$key]); Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: return strtr($string, $args); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Escapes a placeholder replacement value if needed. Chris@0: * Chris@0: * @param string|\Drupal\Component\Render\MarkupInterface $value Chris@0: * A placeholder replacement value. Chris@0: * Chris@0: * @return string Chris@0: * The properly escaped replacement value. Chris@0: */ Chris@0: protected static function placeholderEscape($value) { Chris@0: return $value instanceof MarkupInterface ? (string) $value : Html::escape($value); Chris@0: } Chris@0: Chris@0: }