Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 6 |
CRAP | |
8.63% |
12 / 139 |
| MailFormatHelper | |
0.00% |
0 / 1 |
|
0.00% |
0 / 6 |
2278.10 | |
8.63% |
12 / 139 |
| wrapMail | |
0.00% |
0 / 1 |
2.00 | |
92.31% |
12 / 13 |
|||
| htmlToText | |
0.00% |
0 / 1 |
1482 | |
0.00% |
0 / 100 |
|||
| wrapMailLine | |
0.00% |
0 / 1 |
42 | |
0.00% |
0 / 10 |
|||
| htmlToMailUrls | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 10 |
|||
| htmlToTextClean | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
| htmlToTextPad | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 5 |
|||
| <?php | |
| /** | |
| * @file | |
| * Contains \Drupal\Core\Mail\MailFormatHelper. | |
| */ | |
| namespace Drupal\Core\Mail; | |
| use Drupal\Component\Utility\Html; | |
| use Drupal\Component\Utility\Unicode; | |
| use Drupal\Component\Utility\Xss; | |
| use Drupal\Core\Site\Settings; | |
| /** | |
| * Defines a class containing utility methods for formatting mail messages. | |
| */ | |
| class MailFormatHelper { | |
| /** | |
| * Internal array of urls replaced with tokens. | |
| * | |
| * @var array | |
| */ | |
| protected static $urls = array(); | |
| /** | |
| * Quoted regex expression based on base path. | |
| * | |
| * @var string | |
| */ | |
| protected static $regexp; | |
| /** | |
| * Array of tags supported. | |
| * | |
| * @var array | |
| */ | |
| protected static $supportedTags = array(); | |
| /** | |
| * Performs format=flowed soft wrapping for mail (RFC 3676). | |
| * | |
| * We use delsp=yes wrapping, but only break non-spaced languages when | |
| * absolutely necessary to avoid compatibility issues. | |
| * | |
| * We deliberately use LF rather than CRLF, see MailManagerInterface::mail(). | |
| * | |
| * @param string $text | |
| * The plain text to process. | |
| * @param string $indent | |
| * (optional) A string to indent the text with. Only '>' characters are | |
| * repeated on subsequent wrapped lines. Others are replaced by spaces. | |
| * | |
| * @return string | |
| * The content of the email as a string with formatting applied. | |
| */ | |
| public static function wrapMail($text, $indent = '') { | |
| // Convert CRLF into LF. | |
| $text = str_replace("\r", '', $text); | |
| // See if soft-wrapping is allowed. | |
| $clean_indent = static::htmlToTextClean($indent); | |
| $soft = strpos($clean_indent, ' ') === FALSE; | |
| // Check if the string has line breaks. | |
| if (strpos($text, "\n") !== FALSE) { | |
| // Remove trailing spaces to make existing breaks hard, but leave | |
| // signature marker untouched (RFC 3676, Section 4.3). | |
| $text = preg_replace('/(?(?<!^--) +\n| +\n)/m', "\n", $text); | |
| // Wrap each line at the needed width. | |
| $lines = explode("\n", $text); | |
| array_walk($lines, '\Drupal\Core\Mail\MailFormatHelper::wrapMailLine', array('soft' => $soft, 'length' => strlen($indent))); | |
| $text = implode("\n", $lines); | |
| } | |
| else { | |
| // Wrap this line. | |
| static::wrapMailLine($text, 0, array('soft' => $soft, 'length' => strlen($indent))); | |
| } | |
| // Empty lines with nothing but spaces. | |
| $text = preg_replace('/^ +\n/m', "\n", $text); | |
| // Space-stuff special lines. | |
| $text = preg_replace('/^(>| |From)/m', ' $1', $text); | |
| // Apply indentation. We only include non-'>' indentation on the first line. | |
| $text = $indent . substr(preg_replace('/^/m', $clean_indent, $text), strlen($indent)); | |
| return $text; | |
| } | |
| /** | |
| * Transforms an HTML string into plain text, preserving its structure. | |
| * | |
| * The output will be suitable for use as 'format=flowed; delsp=yes' text | |
| * (RFC 3676) and can be passed directly to MailManagerInterface::mail() for sending. | |
| * | |
| * We deliberately use LF rather than CRLF, see MailManagerInterface::mail(). | |
| * | |
| * This function provides suitable alternatives for the following tags: | |
| * <a> <em> <i> <strong> <b> <br> <p> <blockquote> <ul> <ol> <li> <dl> <dt> | |
| * <dd> <h1> <h2> <h3> <h4> <h5> <h6> <hr> | |
| * | |
| * @param string $string | |
| * The string to be transformed. | |
| * @param array $allowed_tags | |
| * (optional) If supplied, a list of tags that will be transformed. If | |
| * omitted, all supported tags are transformed. | |
| * | |
| * @return string | |
| * The transformed string. | |
| */ | |
| public static function htmlToText($string, $allowed_tags = NULL) { | |
| // Cache list of supported tags. | |
| if (empty(static::$supportedTags)) { | |
| static::$supportedTags = array('a', 'em', 'i', 'strong', 'b', 'br', 'p', | |
| 'blockquote', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', | |
| 'h4', 'h5', 'h6', 'hr'); | |
| } | |
| // Make sure only supported tags are kept. | |
| $allowed_tags = isset($allowed_tags) ? array_intersect(static::$supportedTags, $allowed_tags) : static::$supportedTags; | |
| // Make sure tags, entities and attributes are well-formed and properly | |
| // nested. | |
| $string = Html::normalize(Xss::filter($string, $allowed_tags)); | |
| // Apply inline styles. | |
| $string = preg_replace('!</?(em|i)((?> +)[^>]*)?>!i', '/', $string); | |
| $string = preg_replace('!</?(strong|b)((?> +)[^>]*)?>!i', '*', $string); | |
| // Replace inline <a> tags with the text of link and a footnote. | |
| // 'See <a href="https://www.drupal.org">the Drupal site</a>' becomes | |
| // 'See the Drupal site [1]' with the URL included as a footnote. | |
| static::htmlToMailUrls(NULL, TRUE); | |
| $pattern = '@(<a[^>]+?href="([^"]*)"[^>]*?>(.+?)</a>)@i'; | |
| $string = preg_replace_callback($pattern, 'static::htmlToMailUrls', $string); | |
| $urls = static::htmlToMailUrls(); | |
| $footnotes = ''; | |
| if (count($urls)) { | |
| $footnotes .= "\n"; | |
| for ($i = 0, $max = count($urls); $i < $max; $i++) { | |
| $footnotes .= '[' . ($i + 1) . '] ' . $urls[$i] . "\n"; | |
| } | |
| } | |
| // Split tags from text. | |
| $split = preg_split('/<([^>]+?)>/', $string, -1, PREG_SPLIT_DELIM_CAPTURE); | |
| // Note: PHP ensures the array consists of alternating delimiters and | |
| // literals and begins and ends with a literal (inserting $null as | |
| // required). | |
| // Odd/even counter (tag or no tag). | |
| $tag = FALSE; | |
| // Case conversion function. | |
| $casing = NULL; | |
| $output = ''; | |
| // All current indentation string chunks. | |
| $indent = array(); | |
| // Array of counters for opened lists. | |
| $lists = array(); | |
| foreach ($split as $value) { | |
| // Holds a string ready to be formatted and output. | |
| $chunk = NULL; | |
| // Process HTML tags (but don't output any literally). | |
| if ($tag) { | |
| list($tagname) = explode(' ', strtolower($value), 2); | |
| switch ($tagname) { | |
| // List counters. | |
| case 'ul': | |
| array_unshift($lists, '*'); | |
| break; | |
| case 'ol': | |
| array_unshift($lists, 1); | |
| break; | |
| case '/ul': | |
| case '/ol': | |
| array_shift($lists); | |
| // Ensure blank new-line. | |
| $chunk = ''; | |
| break; | |
| // Quotation/list markers, non-fancy headers. | |
| case 'blockquote': | |
| // Format=flowed indentation cannot be mixed with lists. | |
| $indent[] = count($lists) ? ' "' : '>'; | |
| break; | |
| case 'li': | |
| $indent[] = isset($lists[0]) && is_numeric($lists[0]) ? ' ' . $lists[0]++ . ') ' : ' * '; | |
| break; | |
| case 'dd': | |
| $indent[] = ' '; | |
| break; | |
| case 'h3': | |
| $indent[] = '.... '; | |
| break; | |
| case 'h4': | |
| $indent[] = '.. '; | |
| break; | |
| case '/blockquote': | |
| if (count($lists)) { | |
| // Append closing quote for inline quotes (immediately). | |
| $output = rtrim($output, "> \n") . "\"\n"; | |
| // Ensure blank new-line. | |
| $chunk = ''; | |
| } | |
| // Fall-through. | |
| case '/li': | |
| case '/dd': | |
| array_pop($indent); | |
| break; | |
| case '/h3': | |
| case '/h4': | |
| array_pop($indent); | |
| case '/h5': | |
| case '/h6': | |
| // Ensure blank new-line. | |
| $chunk = ''; | |
| break; | |
| // Fancy headers. | |
| case 'h1': | |
| $indent[] = '======== '; | |
| $casing = '\Drupal\Component\Utility\Unicode::strtoupper'; | |
| break; | |
| case 'h2': | |
| $indent[] = '-------- '; | |
| $casing = '\Drupal\Component\Utility\Unicode::strtoupper'; | |
| break; | |
| case '/h1': | |
| case '/h2': | |
| $casing = NULL; | |
| // Pad the line with dashes. | |
| $output = static::htmlToTextPad($output, ($tagname == '/h1') ? '=' : '-', ' '); | |
| array_pop($indent); | |
| // Ensure blank new-line. | |
| $chunk = ''; | |
| break; | |
| // Horizontal rulers. | |
| case 'hr': | |
| // Insert immediately. | |
| $output .= static::wrapMail('', implode('', $indent)) . "\n"; | |
| $output = static::htmlToTextPad($output, '-'); | |
| break; | |
| // Paragraphs and definition lists. | |
| case '/p': | |
| case '/dl': | |
| // Ensure blank new-line. | |
| $chunk = ''; | |
| break; | |
| } | |
| } | |
| // Process blocks of text. | |
| else { | |
| // Convert inline HTML text to plain text; not removing line-breaks or | |
| // white-space, since that breaks newlines when sanitizing plain-text. | |
| $value = trim(Html::decodeEntities($value)); | |
| if (Unicode::strlen($value)) { | |
| $chunk = $value; | |
| } | |
| } | |
| // See if there is something waiting to be output. | |
| if (isset($chunk)) { | |
| // Apply any necessary case conversion. | |
| if (isset($casing)) { | |
| $chunk = call_user_func($casing, $chunk); | |
| } | |
| $line_endings = Settings::get('mail_line_endings', PHP_EOL); | |
| // Format it and apply the current indentation. | |
| $output .= static::wrapMail($chunk, implode('', $indent)) . $line_endings; | |
| // Remove non-quotation markers from indentation. | |
| $indent = array_map('\Drupal\Core\Mail\MailFormatHelper::htmlToTextClean', $indent); | |
| } | |
| $tag = !$tag; | |
| } | |
| return $output . $footnotes; | |
| } | |
| /** | |
| * Wraps words on a single line. | |
| * | |
| * Callback for array_walk() within | |
| * \Drupal\Core\Mail\MailFormatHelper::wrapMail(). | |
| * | |
| * Note that we are skipping MIME content header lines, because attached | |
| * files, especially applications, could have long MIME types or long | |
| * filenames which result in line length longer than the 77 characters limit | |
| * and wrapping that line will break the email format. For instance, the | |
| * attached file hello_drupal.docx will produce the following Content-Type: | |
| * @code | |
| * Content-Type: | |
| * application/vnd.openxmlformats-officedocument.wordprocessingml.document; | |
| * name="hello_drupal.docx" | |
| * @endcode | |
| */ | |
| protected static function wrapMailLine(&$line, $key, $values) { | |
| $line_is_mime_header = FALSE; | |
| $mime_headers = array( | |
| 'Content-Type', | |
| 'Content-Transfer-Encoding', | |
| 'Content-Disposition', | |
| 'Content-Description', | |
| ); | |
| // Do not break MIME headers which could be longer than 77 characters. | |
| foreach ($mime_headers as $header) { | |
| if (strpos($line, $header . ': ') === 0) { | |
| $line_is_mime_header = TRUE; | |
| break; | |
| } | |
| } | |
| if (!$line_is_mime_header) { | |
| // Use soft-breaks only for purely quoted or unindented text. | |
| $line = wordwrap($line, 77 - $values['length'], $values['soft'] ? " \n" : "\n"); | |
| } | |
| // Break really long words at the maximum width allowed. | |
| $line = wordwrap($line, 996 - $values['length'], $values['soft'] ? " \n" : "\n", TRUE); | |
| } | |
| /** | |
| * Keeps track of URLs and replaces them with placeholder tokens. | |
| * | |
| * Callback for preg_replace_callback() within | |
| * \Drupal\Core\Mail\MailFormatHelper::htmlToText(). | |
| */ | |
| protected static function htmlToMailUrls($match = NULL, $reset = FALSE) { | |
| // @todo Use request context instead. | |
| global $base_url, $base_path; | |
| if ($reset) { | |
| // Reset internal URL list. | |
| static::$urls = array(); | |
| } | |
| else { | |
| if (empty(static::$regexp)) { | |
| static::$regexp = '@^' . preg_quote($base_path, '@') . '@'; | |
| } | |
| if ($match) { | |
| list(, , $url, $label) = $match; | |
| // Ensure all URLs are absolute. | |
| static::$urls[] = strpos($url, '://') ? $url : preg_replace(static::$regexp, $base_url . '/', $url); | |
| return $label . ' [' . count(static::$urls) . ']'; | |
| } | |
| } | |
| return static::$urls; | |
| } | |
| /** | |
| * Replaces non-quotation markers from a piece of indentation with spaces. | |
| * | |
| * Callback for array_map() within | |
| * \Drupal\Core\Mail\MailFormatHelper::htmlToText(). | |
| */ | |
| protected static function htmlToTextClean($indent) { | |
| return preg_replace('/[^>]/', ' ', $indent); | |
| } | |
| /** | |
| * Pads the last line with the given character. | |
| * | |
| * @param string $text | |
| * The text to pad. | |
| * @param string $pad | |
| * The character to pad the end of the string with. | |
| * @param string $prefix | |
| * (optional) Prefix to add to the string. | |
| * | |
| * @return string | |
| * The padded string. | |
| * | |
| * @see \Drupal\Core\Mail\MailFormatHelper::htmlToText() | |
| */ | |
| protected static function htmlToTextPad($text, $pad, $prefix = '') { | |
| // Remove last line break. | |
| $text = substr($text, 0, -1); | |
| // Calculate needed padding space and add it. | |
| if (($p = strrpos($text, "\n")) === FALSE) { | |
| $p = -1; | |
| } | |
| $n = max(0, 79 - (strlen($text) - $p) - strlen($prefix)); | |
| // Add prefix and padding, and restore linebreak. | |
| return $text . $prefix . str_repeat($pad, $n) . "\n"; | |
| } | |
| } |