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' => $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: }